From 98220ffee1a654f10d7e3ef77c6758e9c1e79dc3 Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Wed, 28 Aug 2024 23:02:01 +0200 Subject: [PATCH 01/11] Add VaultStatus and OnboardingStatus messages Signed-off-by: Mikhail Malyshev --- src/ipc/eve_types.rs | 37 +++++++++++++++++++++++++++++++++++++ src/ipc/message.rs | 6 +++++- src/ipc/tests.rs | 37 ++++++++++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/ipc/eve_types.rs b/src/ipc/eve_types.rs index 50bac5f..79b8426 100644 --- a/src/ipc/eve_types.rs +++ b/src/ipc/eve_types.rs @@ -1269,3 +1269,40 @@ pub struct IoAdapter {} // Replace with actual definition #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)] pub struct SnapshottingStatus {} // Replace with actual definition + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct OnboardingStatus { + #[serde(rename = "DeviceUUID")] + pub device_uuid: Uuid, + pub hardware_model: String, // From controller +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct VaultStatus { + pub name: String, + pub status: DataSecAtRestStatus, + #[serde(rename = "PCRStatus")] + pub pcr_status: PCRStatus, + pub conversion_complete: bool, + #[serde(flatten)] + pub error_and_time: ErrorAndTime, // Unknown type, skipped +} + +#[repr(i32)] +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Clone)] +pub enum DataSecAtRestStatus { + DataSecAtRestUnknown = 0, // Status is unknown + DataSecAtRestDisabled = 1, // Enabled, but not being used + DataSecAtRestEnabled = 2, // Enabled, and used + DataSecAtRestError = 4, // Enabled, but encountered an error +} + +#[repr(i32)] +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Clone)] +pub enum PCRStatus { + PcrUnknown = 0, // Status is unknown + PcrEnabled = 1, // Enabled PCR + PcrDisabled = 2, // Disabled PCR +} diff --git a/src/ipc/message.rs b/src/ipc/message.rs index fa8a5a4..9b75553 100644 --- a/src/ipc/message.rs +++ b/src/ipc/message.rs @@ -14,7 +14,9 @@ use super::eve_types::DeviceNetworkStatus; use super::eve_types::DevicePortConfig; use super::eve_types::DevicePortConfigList; use super::eve_types::DownloaderStatus; +use super::eve_types::OnboardingStatus; use super::eve_types::PhysicalIOAdapterList; +use super::eve_types::VaultStatus; /// WindowId is a unique identifier for a window that is incremented sequentially. pub type RequestId = u64; @@ -45,12 +47,14 @@ pub enum IpcMessage { DownloaderStatus(DownloaderStatus), IOAdapters(PhysicalIOAdapterList), AppStatus(AppInstanceStatus), + VaultStatus(VaultStatus), + OnboardingStatus(OnboardingStatus), Response { #[serde(flatten)] result: core::result::Result, id: u64, }, - #[serde(untagged)] + // #[serde(untagged)] Request { #[serde(flatten)] request: Request, diff --git a/src/ipc/tests.rs b/src/ipc/tests.rs index a174d76..0731116 100644 --- a/src/ipc/tests.rs +++ b/src/ipc/tests.rs @@ -3140,5 +3140,40 @@ fn test_app_data_full() { } } "#; - let a: IpcMessage = serde_json::from_str(json_data).unwrap(); + let _: IpcMessage = serde_json::from_str(json_data).unwrap(); +} + +#[test] +fn test_onboarding_status() { + let json_data = r#" + { + "type": "OnboardingStatus", + "message": { + "DeviceUUID": "584a643c-c8b1-45d5-a287-84fc81e0d39c", + "HardwareModel": "QEMU.Standard PC (Q35 + ICH9, 2009)" + } + } + "#; + let _: IpcMessage = serde_json::from_str(json_data).unwrap(); +} + +#[test] +fn test_vault_status() { + let json_data = r#" + { + "type": "VaultStatus", + "message": { + "Name": "Application Data Store", + "Status": 1, + "PCRStatus": 2, + "ConversionComplete": true, + "Error": "TPM is either absent or not in use", + "ErrorTime": "2024-08-28T20:15:08.246947098Z", + "ErrorSeverity": 3, + "ErrorRetryCondition": "", + "ErrorEntities": null + } + } + "#; + let _: IpcMessage = serde_json::from_str(json_data).unwrap(); } From 6c4fab4a60ce591cd74bcadc5280b4ef40288384 Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Thu, 29 Aug 2024 00:28:04 +0200 Subject: [PATCH 02/11] Add AppSummary message Signed-off-by: Mikhail Malyshev --- src/ipc/eve_types.rs | 13 +++++++++++++ src/ipc/message.rs | 4 +++- src/ipc/tests.rs | 20 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/ipc/eve_types.rs b/src/ipc/eve_types.rs index 79b8426..06cc039 100644 --- a/src/ipc/eve_types.rs +++ b/src/ipc/eve_types.rs @@ -1306,3 +1306,16 @@ pub enum PCRStatus { PcrEnabled = 1, // Enabled PCR PcrDisabled = 2, // Disabled PCR } + +type AppCount = u8; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct AppInstanceSummary { + #[serde(rename = "UUIDandVersion")] + pub uuid_and_version: UUIDandVersion, + pub total_starting: AppCount, // Total number of apps starting/booting + pub total_running: AppCount, // Total number of apps in running state + pub total_stopping: AppCount, // Total number of apps in halting state + pub total_error: AppCount, // Total number of apps in error state +} diff --git a/src/ipc/message.rs b/src/ipc/message.rs index 9b75553..da321bd 100644 --- a/src/ipc/message.rs +++ b/src/ipc/message.rs @@ -10,6 +10,7 @@ use serde::Deserialize; use serde::Serialize; use super::eve_types::AppInstanceStatus; +use super::eve_types::AppInstanceSummary; use super::eve_types::DeviceNetworkStatus; use super::eve_types::DevicePortConfig; use super::eve_types::DevicePortConfigList; @@ -47,6 +48,7 @@ pub enum IpcMessage { DownloaderStatus(DownloaderStatus), IOAdapters(PhysicalIOAdapterList), AppStatus(AppInstanceStatus), + AppSummary(AppInstanceSummary), VaultStatus(VaultStatus), OnboardingStatus(OnboardingStatus), Response { @@ -54,7 +56,7 @@ pub enum IpcMessage { result: core::result::Result, id: u64, }, - // #[serde(untagged)] + #[serde(untagged)] Request { #[serde(flatten)] request: Request, diff --git a/src/ipc/tests.rs b/src/ipc/tests.rs index 0731116..3b597f7 100644 --- a/src/ipc/tests.rs +++ b/src/ipc/tests.rs @@ -3177,3 +3177,23 @@ fn test_vault_status() { "#; let _: IpcMessage = serde_json::from_str(json_data).unwrap(); } + +#[test] +fn test_app_summary() { + let json_data = r#" + { + "type": "AppSummary", + "message": { + "UUIDandVersion": { + "UUID": "00000000-0000-0000-0000-000000000000", + "Version": "" + }, + "TotalStarting": 1, + "TotalRunning": 0, + "TotalStopping": 0, + "TotalError": 0 + } + } + "#; + let _: IpcMessage = serde_json::from_str(json_data).unwrap(); +} From 3b0e9a4627dd288c8fdcfb058bcb885ec12de432 Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Sun, 1 Sep 2024 20:19:55 +0200 Subject: [PATCH 03/11] Clear whole screen but only ones Signed-off-by: Mikhail Malyshev --- src/ui/ui.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ui/ui.rs b/src/ui/ui.rs index e0c365e..44cdb46 100644 --- a/src/ui/ui.rs +++ b/src/ui/ui.rs @@ -55,6 +55,7 @@ pub struct Ui { pub views: Vec, pub selected_tab: UiTabs, pub status_bar: Window, + first_frame: bool, } #[derive(Default, Copy, Clone, Display, EnumIter, Debug, FromRepr, EnumCount)] @@ -81,6 +82,7 @@ impl Ui { views: vec![LayerStack::new(); UiTabs::COUNT], selected_tab: UiTabs::default(), status_bar: create_status_bar(), + first_frame: true, }) } @@ -236,11 +238,14 @@ impl Ui { let area = frame.size(); let [tabs, body, statusbar_rect] = screen_layout.areas(area); + if self.first_frame { + self.first_frame = false; + frame.render_widget(Clear, area); + } tabs_widget .select(self.selected_tab as usize) .render(tabs, frame.buffer_mut()); - Clear.render(body, frame.buffer_mut()); // redraw from the bottom up let stack = &mut self.views[self.selected_tab as usize]; From be9d8f1387eab3b2dc4d142240d2d142d67efad4 Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Sun, 1 Sep 2024 20:23:05 +0200 Subject: [PATCH 04/11] Move ISelector trait to ui/traits.rs Signed-off-by: Mikhail Malyshev --- src/ui/networkpage.rs | 13 ++++--------- src/ui/traits.rs | 7 +++++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ui/networkpage.rs b/src/ui/networkpage.rs index 0f4b9c4..e8af66c 100644 --- a/src/ui/networkpage.rs +++ b/src/ui/networkpage.rs @@ -19,7 +19,10 @@ use crate::{ traits::{IEventHandler, IPresenter, IWindow}, }; -use super::action::{Action, UiActions}; +use super::{ + action::{Action, UiActions}, + traits::ISelector, +}; const MAC_LENGTH: u16 = 17; const LINK_STATE_LENGTH: u16 = 4; @@ -302,14 +305,6 @@ impl IEventHandler for NetworkPage { } } -trait ISelector { - fn select_next(&mut self); - fn select_previous(&mut self); - fn select_first(&mut self); - fn select_last(&mut self); - fn selected(&self) -> Option; -} - impl ISelector for NetworkPage { fn select_next(&mut self) { if let Some(selected) = self.list.state.selected() { diff --git a/src/ui/traits.rs b/src/ui/traits.rs index 49d9267..84da6b4 100644 --- a/src/ui/traits.rs +++ b/src/ui/traits.rs @@ -3,3 +3,10 @@ use ratatui::style::Style; pub trait IntoRatatuiStyle { fn style(&self) -> Style; } +pub trait ISelector { + fn select_next(&mut self); + fn select_previous(&mut self); + fn select_first(&mut self); + fn select_last(&mut self); + fn selected(&self) -> Option; +} From cc7433bc299be8e20d24b77ee1d647aba6cada8b Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Sun, 1 Sep 2024 20:28:29 +0200 Subject: [PATCH 05/11] Add more EVE IPC messages and corresponding tests Signed-off-by: Mikhail Malyshev --- src/ipc/eve_types.rs | 68 +++++++++++++-- src/ipc/message.rs | 15 +++- src/ipc/tests.rs | 200 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 273 insertions(+), 10 deletions(-) diff --git a/src/ipc/eve_types.rs b/src/ipc/eve_types.rs index 06cc039..04eeb35 100644 --- a/src/ipc/eve_types.rs +++ b/src/ipc/eve_types.rs @@ -7,6 +7,7 @@ use macaddr::MacAddr8; use serde::de; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::collections::HashMap; use std::default; use std::net::IpAddr; use strum::Display; @@ -963,6 +964,7 @@ pub enum ErrorSeverity { #[serde(rename_all = "PascalCase")] pub struct ErrorEntity { pub entity_type: ErrorEntityType, + #[serde(rename = "EntityID")] pub entity_id: String, } @@ -1137,6 +1139,11 @@ pub enum SwState { MaxState = 125, } +impl SwState { + pub fn to_string(&self) -> String { + format!("{}", self) + } +} #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[serde(rename_all = "PascalCase")] pub struct UUIDandVersion { @@ -1272,7 +1279,7 @@ pub struct SnapshottingStatus {} // Replace with actual definition #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] -pub struct OnboardingStatus { +pub struct EveOnboardingStatus { #[serde(rename = "DeviceUUID")] pub device_uuid: Uuid, pub hardware_model: String, // From controller @@ -1280,12 +1287,14 @@ pub struct OnboardingStatus { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] -pub struct VaultStatus { +pub struct EveVaultStatus { pub name: String, pub status: DataSecAtRestStatus, #[serde(rename = "PCRStatus")] pub pcr_status: PCRStatus, pub conversion_complete: bool, + #[serde(rename = "MissmatchingPCRs")] + pub missmatching_pcrs: Vec, #[serde(flatten)] pub error_and_time: ErrorAndTime, // Unknown type, skipped } @@ -1309,13 +1318,62 @@ pub enum PCRStatus { type AppCount = u8; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "PascalCase")] pub struct AppInstanceSummary { - #[serde(rename = "UUIDandVersion")] - pub uuid_and_version: UUIDandVersion, + //#[serde(rename = "UUIDandVersion")] + //pub uuid_and_version: UUIDandVersion, pub total_starting: AppCount, // Total number of apps starting/booting pub total_running: AppCount, // Total number of apps in running state pub total_stopping: AppCount, // Total number of apps in halting state pub total_error: AppCount, // Total number of apps in error state } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct LedBlinkCounter { + pub blink_counter: LedBlinkCount, +} + +#[repr(u8)] +#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Clone)] +pub enum LedBlinkCount { + LedBlinkUndefined = 0, + LedBlinkWaitingForIP, + LedBlinkConnectingToController, + LedBlinkConnectedToController, + LedBlinkOnboarded, + LedBlinkRadioSilence, + LedBlinkOnboardingFailure = 10, + LedBlinkRespWithoutTLS = 12, + LedBlinkRespWithoutOSCP, + LedBlinkInvalidControllerCert, + LedBlinkInvalidAuthContainer, + LedBlinkInvalidBootstrapConfig, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct EveNodeStatus { + pub server: Option, + #[serde(deserialize_with = "zero_uuid_as_none")] + pub node_uuid: Option, + pub onboarded: bool, + pub app_instance_summary: Option, +} + +fn zero_uuid_as_none<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + if s == "00000000-0000-0000-0000-000000000000" { + Ok(None) + } else { + Ok(Some(Uuid::parse_str(&s).map_err(serde::de::Error::custom)?)) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AppsList { + pub apps: Vec, +} diff --git a/src/ipc/message.rs b/src/ipc/message.rs index da321bd..fa9ac0b 100644 --- a/src/ipc/message.rs +++ b/src/ipc/message.rs @@ -8,16 +8,20 @@ use bytes::BytesMut; use log::error; use serde::Deserialize; use serde::Serialize; +use uuid::Uuid; use super::eve_types::AppInstanceStatus; use super::eve_types::AppInstanceSummary; +use super::eve_types::AppsList; use super::eve_types::DeviceNetworkStatus; use super::eve_types::DevicePortConfig; use super::eve_types::DevicePortConfigList; use super::eve_types::DownloaderStatus; -use super::eve_types::OnboardingStatus; +use super::eve_types::EveNodeStatus; +use super::eve_types::EveOnboardingStatus; +use super::eve_types::EveVaultStatus; +use super::eve_types::LedBlinkCounter; use super::eve_types::PhysicalIOAdapterList; -use super::eve_types::VaultStatus; /// WindowId is a unique identifier for a window that is incremented sequentially. pub type RequestId = u64; @@ -49,8 +53,11 @@ pub enum IpcMessage { IOAdapters(PhysicalIOAdapterList), AppStatus(AppInstanceStatus), AppSummary(AppInstanceSummary), - VaultStatus(VaultStatus), - OnboardingStatus(OnboardingStatus), + VaultStatus(EveVaultStatus), + OnboardingStatus(EveOnboardingStatus), + LedBlinkCounter(LedBlinkCounter), + NodeStatus(EveNodeStatus), + AppsList(AppsList), Response { #[serde(flatten)] result: core::result::Result, diff --git a/src/ipc/tests.rs b/src/ipc/tests.rs index 3b597f7..03fd580 100644 --- a/src/ipc/tests.rs +++ b/src/ipc/tests.rs @@ -2,14 +2,19 @@ use std::net::IpAddr; use std::str::FromStr; use super::*; +use async_inotify::app::parse; +use chrono::DateTime; +use chrono::Utc; use eve_types::deserialize_mac; use eve_types::AppInstanceStatus; use eve_types::BondConfig; use eve_types::BondMode; use eve_types::DPCState; +use eve_types::DataSecAtRestStatus; use eve_types::DeviceNetworkStatus; use eve_types::DevicePortConfigList; use eve_types::DhcpType; +use eve_types::ErrorSeverity; use eve_types::GoIpNetwork; use eve_types::LacpRate; use eve_types::MIIMonitor; @@ -3046,7 +3051,7 @@ fn test_network_port_status_dns() { } #[test] -fn test_app_data_full() { +fn test_app_status_full() { let json_data = r#" { "type": "AppStatus", @@ -3178,6 +3183,46 @@ fn test_vault_status() { let _: IpcMessage = serde_json::from_str(json_data).unwrap(); } +#[test] +fn test_vault_status_error() { + let json_data = r#" + { + "type": "VaultStatus", + "message": { + "Name": "Application Data Store", + "Status": 4, + "PCRStatus": 1, + "ConversionComplete": false, + "MissmatchingPCRs": [8, 13], + "Error": "Vault key unavailable", + "ErrorTime": "2024-08-31T18:54:42.220924775Z", + "ErrorSeverity": 3, + "ErrorRetryCondition": "", + "ErrorEntities": null + } + } + "#; + let msg: IpcMessage = serde_json::from_str(json_data).unwrap(); + match msg { + IpcMessage::VaultStatus(eve_status) => { + assert_eq!(eve_status.status, DataSecAtRestStatus::DataSecAtRestError); + assert_eq!( + eve_status.error_and_time.error_description.error, + "Vault key unavailable" + ); + // assert_eq!( + // eve_status.error_and_time.error_description.error_time, + // DateTime::parse::("2024-08-31T18:54:42.220924775Z").unwrap() + // ); + assert_eq!( + eve_status.error_and_time.error_description.error_severity, + ErrorSeverity::Error + ); + } + _ => panic!("unexpected message type"), + } +} + #[test] fn test_app_summary() { let json_data = r#" @@ -3197,3 +3242,156 @@ fn test_app_summary() { "#; let _: IpcMessage = serde_json::from_str(json_data).unwrap(); } + +#[test] +fn test_node_status_not_onboarded() { + let json_data = r#" + { + "type": "NodeStatus", + "message": { + "server": "zedcloud.alpha.zededa.net", + "node_uuid": "00000000-0000-0000-0000-000000000000", + "onboarded": false + } + } + "#; + let msg: IpcMessage = serde_json::from_str(json_data).unwrap(); + match msg { + IpcMessage::NodeStatus(message) => { + assert_eq!(message.onboarded, false); + assert_eq!(message.node_uuid, None); + assert_eq!( + message.server, + Some("zedcloud.alpha.zededa.net".to_string()) + ) + } + _ => panic!("unexpected message type"), + } +} + +#[test] +fn test_node_status_not_onboarded_emty_server() { + let json_data = r#" + { + "type": "NodeStatus", + "message": { + "node_uuid": "00000000-0000-0000-0000-000000000000", + "onboarded": false + } + } + "#; + let msg: IpcMessage = serde_json::from_str(json_data).unwrap(); + match msg { + IpcMessage::NodeStatus(message) => { + assert_eq!(message.onboarded, false); + assert_eq!(message.node_uuid, None); + assert_eq!(message.server, None) + } + _ => panic!("unexpected message type"), + } +} + +#[test] +fn test_app_status_with_error() { + let json_data = r#" + { + "type": "AppStatus", + "message": { + "UUIDandVersion": { + "UUID": "68cec03f-3c11-4dbe-8089-5d6f3dc3c872", + "Version": "1" + }, + "DisplayName": "cs_nginx-qemu-1", + "DomainName": "", + "Activated": false, + "ActivateInprogress": false, + "FixedResources": { + "Kernel": "", + "Ramdisk": "", + "Memory": 524288, + "MaxMem": 524288, + "VCpus": 1, + "MaxCpus": 0, + "RootDev": "/dev/xvda1", + "ExtraArgs": "", + "BootLoader": "/usr/bin/pygrub", + "CPUs": "", + "DeviceTree": "", + "DtDev": null, + "IRQs": null, + "IOMem": null, + "VirtualizationMode": 0, + "EnableVnc": true, + "VncDisplay": 0, + "VncPasswd": "", + "CPUsPinned": false, + "VMMMaxMem": 0, + "EnableVncShimVM": false + }, + "VolumeRefStatusList": [ + { + "VolumeID": "245238bd-9cb6-4601-a77a-43d21a0f1c2f", + "GenerationCounter": 0, + "LocalGenerationCounter": 0, + "AppUUID": "68cec03f-3c11-4dbe-8089-5d6f3dc3c872", + "State": 101, + "ActiveFileLocation": "", + "ContentFormat": 0, + "ReadOnly": false, + "DisplayName": "cs_nginx-qemu-1_0_m_0", + "MaxVolSize": 0, + "PendingAdd": false, + "WWN": "", + "VerifyOnly": true, + "Target": 0, + "CustomMeta": "", + "ReferenceName": "", + "ErrorSourceType": "types.VolumeStatus", + "Error": "Found error in content tree cs_nginx_image attached to volume cs_nginx-qemu-1_0_m_0: lookupDatastoreConfig(c1b0cb8a-9b39-4b9a-82ff-b5409757ecc2) error: Get(zedagent/DatastoreConfig) unknown key c1b0cb8a-9b39-4b9a-82ff-b5409757ecc2", + "ErrorTime": "2024-08-31T10:17:58.660422709Z", + "ErrorSeverity": 1, + "ErrorRetryCondition": "Will retry when datastore available", + "ErrorEntities": [ + { + "EntityType": 11, + "EntityID": "245238bd-9cb6-4601-a77a-43d21a0f1c2f" + } + ] + } + ], + "AppNetAdapters": null, + "BootTime": "0001-01-01T00:00:00Z", + "IoAdapterList": null, + "RestartInprogress": 0, + "RestartStartedAt": "0001-01-01T00:00:00Z", + "PurgeInprogress": 0, + "PurgeStartedAt": "0001-01-01T00:00:00Z", + "State": 101, + "MissingNetwork": false, + "MissingMemory": false, + "ErrorSourceType": "types.VolumeStatus", + "Error": "Found error in content tree cs_nginx_image attached to volume cs_nginx-qemu-1_0_m_0: lookupDatastoreConfig(c1b0cb8a-9b39-4b9a-82ff-b5409757ecc2) error: Get(zedagent/DatastoreConfig) unknown key c1b0cb8a-9b39-4b9a-82ff-b5409757ecc2\n\n", + "ErrorTime": "2024-08-31T10:17:58.660422709Z", + "ErrorSeverity": 1, + "ErrorRetryCondition": "Will retry when datastore available", + "ErrorEntities": [ + { "EntityType": 11, "EntityID": "245238bd-9cb6-4601-a77a-43d21a0f1c2f" } + ], + "StartTime": "2024-08-31T10:17:51.591626391Z", + "SnapStatus": { + "MaxSnapshots": 1, + "RequestedSnapshots": null, + "AvailableSnapshots": null, + "SnapshotsToBeDeleted": null, + "PreparedVolumesSnapshotConfigs": null, + "SnapshotOnUpgrade": false, + "HasRollbackRequest": false, + "ActiveSnapshot": "", + "RollbackInProgress": false + }, + "MemOverhead": 0 + } + } + "#; + let _msg: IpcMessage = serde_json::from_str(json_data).unwrap(); +} From 99ada03680b311b88445758548f1de586eb5e798 Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Sun, 1 Sep 2024 20:30:21 +0200 Subject: [PATCH 06/11] Exit application on CRTL+e. CTRL+q quits Zellij Signed-off-by: Mikhail Malyshev --- src/events.rs | 2 +- src/ui/ui.rs | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/events.rs b/src/events.rs index 4b52234..3ac9909 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,6 +1,6 @@ use crossterm::event::KeyEvent; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum Event { Key(KeyEvent), Tick, diff --git a/src/ui/ui.rs b/src/ui/ui.rs index 44cdb46..4ad14de 100644 --- a/src/ui/ui.rs +++ b/src/ui/ui.rs @@ -266,12 +266,14 @@ impl Ui { } pub fn handle_event(&mut self, event: Event) -> Option { - debug!("Ui handle_event {:?}", event); + if event != Event::Tick { + debug!("Ui handle_event {:?}", event); + } match event { // only fo debugging purposes Event::Key(key) - if (key.code == KeyCode::Char('q')) && (key.modifiers == KeyModifiers::CONTROL) => + if (key.code == KeyCode::Char('e')) && (key.modifiers == KeyModifiers::CONTROL) => { debug!("CTRL+q: application Quit requested"); self.action_tx @@ -318,20 +320,20 @@ impl Ui { // } // show network edit dialog on ctrl+e - Event::Key(key) - if (key.code == KeyCode::Char('e')) && (key.modifiers == KeyModifiers::CONTROL) => - { - debug!("CTRL+e: show dialog"); + // Event::Key(key) + // if (key.code == KeyCode::Char('e')) && (key.modifiers == KeyModifiers::CONTROL) => + // { + // debug!("CTRL+e: show dialog"); - // let s = IpDialogState { - // ip: "10.208.13.10".to_string(), - // mode: "DHCP".to_string(), - // gw: "1.1.1.1".to_string(), - // }; + // // let s = IpDialogState { + // // ip: "10.208.13.10".to_string(), + // // mode: "DHCP".to_string(), + // // gw: "1.1.1.1".to_string(), + // // }; - // let d: NetworkDialog = NetworkDialog::new(); - // self.views[self.selected_tab as usize].push(Box::new(d)); - } + // // let d: NetworkDialog = NetworkDialog::new(); + // // self.views[self.selected_tab as usize].push(Box::new(d)); + // } // handle Tab switching // Event::Key(key) From be1938fe356f5a6b3fe0eee8979d9ec0c85c9d70 Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Sun, 1 Sep 2024 21:22:57 +0200 Subject: [PATCH 07/11] Move model to new module Signed-off-by: Mikhail Malyshev --- src/application.rs | 3 ++- src/main.rs | 1 - src/{ => model}/device/dmesg.rs | 2 +- src/{ => model}/device/mod.rs | 0 src/{ => model}/device/network.rs | 0 src/{ => model}/device/summary.rs | 1 - src/model/mod.rs | 2 ++ src/{ => model}/model.rs | 3 ++- src/traits.rs | 3 ++- src/ui/dialog.rs | 2 +- src/ui/homepage.rs | 4 ++-- src/ui/ipdialog.rs | 6 ++++-- src/ui/networkpage.rs | 4 ++-- src/ui/ui.rs | 6 +++--- src/ui/window.rs | 2 +- 15 files changed, 22 insertions(+), 17 deletions(-) rename src/{ => model}/device/dmesg.rs (99%) rename src/{ => model}/device/mod.rs (100%) rename src/{ => model}/device/network.rs (100%) rename src/{ => model}/device/summary.rs (98%) create mode 100644 src/model/mod.rs rename src/{ => model}/model.rs (96%) diff --git a/src/application.rs b/src/application.rs index 3b1d150..a4764ed 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,5 +1,6 @@ use crate::events::Event; -use crate::model::{Model, MonitorModel}; +use crate::model::model::Model; +use crate::model::model::MonitorModel; use crate::raw_model::RawModel; use crate::ui::ui::Ui; use core::fmt::Debug; diff --git a/src/main.rs b/src/main.rs index db3476b..81ad3de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ mod actions; mod application; -mod device; mod events; mod ipc; mod mainwnd; diff --git a/src/device/dmesg.rs b/src/model/device/dmesg.rs similarity index 99% rename from src/device/dmesg.rs rename to src/model/device/dmesg.rs index f146e86..683451f 100644 --- a/src/device/dmesg.rs +++ b/src/model/device/dmesg.rs @@ -1,4 +1,4 @@ -use crate::model::Model; +use crate::model::model::Model; use crate::ui::action::Action; use crate::ui::activity::Activity; use crate::ui::traits::IntoRatatuiStyle; diff --git a/src/device/mod.rs b/src/model/device/mod.rs similarity index 100% rename from src/device/mod.rs rename to src/model/device/mod.rs diff --git a/src/device/network.rs b/src/model/device/network.rs similarity index 100% rename from src/device/network.rs rename to src/model/device/network.rs diff --git a/src/device/summary.rs b/src/model/device/summary.rs similarity index 98% rename from src/device/summary.rs rename to src/model/device/summary.rs index 85222ab..d2f6e3b 100644 --- a/src/device/summary.rs +++ b/src/model/device/summary.rs @@ -1,7 +1,6 @@ use chrono::DateTime; use chrono::Utc; -use crate::device::network; use std::process::Command; use super::network::NetworkInterfaceStatus; diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..7f91e89 --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,2 @@ +pub mod device; +pub mod model; diff --git a/src/model.rs b/src/model/model.rs similarity index 96% rename from src/model.rs rename to src/model/model.rs index 13302a8..064a01d 100644 --- a/src/model.rs +++ b/src/model/model.rs @@ -1,11 +1,12 @@ use std::cell::RefCell; use crate::{ - device::network::NetworkInterfaceStatus, ipc::{eve_types::DownloaderStatus, message::IpcMessage}, raw_model::RawModel, }; +use super::device::network::NetworkInterfaceStatus; + pub type Model = RefCell; #[derive(Debug)] pub struct MonitorModel { diff --git a/src/traits.rs b/src/traits.rs index 70b6630..ba308e7 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,8 +1,9 @@ use std::rc::Rc; +use crate::events::Event; +use crate::model::model::Model; use crate::ui::action::{Action, UiActions}; use crate::ui::activity::Activity; -use crate::{events::Event, model::Model}; use log::info; use ratatui::{layout::Rect, Frame}; diff --git a/src/ui/dialog.rs b/src/ui/dialog.rs index b427a94..71eeff0 100644 --- a/src/ui/dialog.rs +++ b/src/ui/dialog.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use crate::events; -use crate::model::Model; +use crate::model::model::Model; use crate::traits::IElementEventHandler; use crate::ui::activity::Activity; use log::debug; diff --git a/src/ui/homepage.rs b/src/ui/homepage.rs index 3f5113b..310d9c9 100644 --- a/src/ui/homepage.rs +++ b/src/ui/homepage.rs @@ -1,5 +1,5 @@ -use crate::device::summary::DeviceSummary; use crate::ipc::eve_types::DownloaderStatus; +use crate::model::device::summary::DeviceSummary; use ratatui::text::Line; use ratatui::text::Text; use ratatui::widgets::Block; @@ -9,7 +9,7 @@ use std::borrow; use std::rc::Rc; use crate::events; -use crate::model::Model; +use crate::model::model::Model; use crate::traits::{IEventHandler, IPresenter, IWindow}; use crate::ui::action::Action; use crate::ui::window::LayoutMap; diff --git a/src/ui/ipdialog.rs b/src/ui/ipdialog.rs index 11a1b25..08188e0 100644 --- a/src/ui/ipdialog.rs +++ b/src/ui/ipdialog.rs @@ -11,8 +11,10 @@ use ratatui::{ use crate::{ actions::MonActions, - device::network::{NetworkInterfaceStatus, ProxyConfig}, - model::Model, + model::{ + device::network::{NetworkInterfaceStatus, ProxyConfig}, + model::Model, + }, traits::IWindow, }; diff --git a/src/ui/networkpage.rs b/src/ui/networkpage.rs index e8af66c..8c907bc 100644 --- a/src/ui/networkpage.rs +++ b/src/ui/networkpage.rs @@ -13,9 +13,9 @@ use ratatui::{ }; use crate::{ - device::network::{NetworkInterfaceStatus, NetworkType}, events::Event, - model::{Model, MonitorModel}, + model::device::network::{NetworkInterfaceStatus, NetworkType}, + model::model::{Model, MonitorModel}, traits::{IEventHandler, IPresenter, IWindow}, }; diff --git a/src/ui/ui.rs b/src/ui/ui.rs index 4ad14de..86a96fb 100644 --- a/src/ui/ui.rs +++ b/src/ui/ui.rs @@ -1,5 +1,5 @@ use crate::{ - device::network::NetworkInterfaceStatus, + model::device::network::NetworkInterfaceStatus, traits::{IPresenter, IWindow}, ui::ipdialog::create_ip_dialog, }; @@ -21,9 +21,9 @@ use tokio::sync::mpsc::UnboundedSender; use crate::{ actions::{MainWndState, MonActions}, - device::dmesg::DmesgViewer, events::Event, - model::Model, + model::device::dmesg::DmesgViewer, + model::model::Model, terminal::TerminalWrapper, traits::IEventHandler, ui::action::UiActions, diff --git a/src/ui/window.rs b/src/ui/window.rs index a63de2d..a0b933d 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -1,5 +1,5 @@ use crate::events; -use crate::model::Model; +use crate::model::model::Model; use std::borrow::BorrowMut; use std::collections::HashMap; use std::{fmt::Debug, rc::Rc}; From 47cf54da6dabc2b2767047410503626e93b72acd Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Sun, 1 Sep 2024 22:50:44 +0200 Subject: [PATCH 08/11] Get rid of RawModel (allmost) Signed-off-by: Mikhail Malyshev --- src/application.rs | 43 ++++++++-- src/model/model.rs | 196 +++++++++++++++++++++++++++++++++++++++++---- src/raw_model.rs | 38 ++------- 3 files changed, 224 insertions(+), 53 deletions(-) diff --git a/src/application.rs b/src/application.rs index a4764ed..c377d31 100644 --- a/src/application.rs +++ b/src/application.rs @@ -92,24 +92,55 @@ impl Application { } IpcMessage::NetworkStatus(cfg) => { debug!("Got Network status"); - self.raw_model.set_network_status(cfg); + self.model.borrow_mut().update_network_status(cfg); } IpcMessage::AppStatus(app) => { debug!("Got AppStatus"); + self.model.borrow_mut().update_app_status(app); } - IpcMessage::DownloaderStatus(cfg) => { - self.raw_model.set_downloader_status(cfg); + IpcMessage::DownloaderStatus(dnl) => { debug!("Got DownloaderStatus"); + self.model.borrow_mut().update_downloader_status(dnl); + } + + // this event is guaranteed to be sent before periodic events + IpcMessage::AppSummary(summary) => { + debug!("Got AppSummary"); + self.model.borrow_mut().update_app_summary(summary); + } + + // this event is guaranteed to be sent before periodic events + IpcMessage::NodeStatus(node_status) => { + debug!("Got NodeStatus"); + self.model.borrow_mut().update_node_status(node_status); + } + + IpcMessage::OnboardingStatus(o_status) => { + debug!("Got OnboardingStatus"); + self.model.borrow_mut().update_onboarding_status(o_status); + } + + IpcMessage::VaultStatus(status) => { + debug!("Got VaultStatus"); + self.model.borrow_mut().update_vault_status(status); + } + + IpcMessage::LedBlinkCounter(led) => { + // self.raw_model.set_led_blink_counter(led); + debug!("Got LedBlinkCounter"); + } + + // this event is guaranteed to be sent before periodic events + IpcMessage::AppsList(app_list) => { + debug!("Got AppsList"); + self.model.borrow_mut().update_app_list(app_list); } _ => { warn!("Unhandled IPC message: {:?}", msg); } } - self.model - .borrow_mut() - .update_from_raw_model(&self.raw_model); } pub fn send_dpc(&self) { diff --git a/src/model/model.rs b/src/model/model.rs index 064a01d..bc6af4b 100644 --- a/src/model/model.rs +++ b/src/model/model.rs @@ -1,35 +1,197 @@ -use std::cell::RefCell; +use std::{cell::RefCell, collections::HashMap}; -use crate::{ - ipc::{eve_types::DownloaderStatus, message::IpcMessage}, - raw_model::RawModel, +use chrono::{DateTime, Utc}; +use uuid::Uuid; + +use crate::ipc::eve_types::{ + AppInstanceStatus, AppInstanceSummary, AppsList, DataSecAtRestStatus, DeviceNetworkStatus, + DownloaderStatus, ErrorAndTime, EveNodeStatus, EveOnboardingStatus, EveVaultStatus, PCRStatus, + SwState, }; use super::device::network::NetworkInterfaceStatus; +#[derive(Debug, Clone, Default)] +pub enum OnboardingStatus { + #[default] + Unknown, + Onboarding, + Onboarded(Uuid), + Error(String), +} + +#[derive(Debug, Default)] +pub struct NodeStatus { + pub server: Option, + pub app_summary: AppInstanceSummary, + pub onboarding_status: OnboardingStatus, +} + +#[derive(Debug)] +pub enum AppInstanceState { + Normal(SwState), + Error(SwState, String), +} + +#[derive(Debug)] +pub struct AppInstance { + pub name: String, + pub uuid: Uuid, + pub version: String, + pub state: AppInstanceState, +} + +#[derive(Debug)] +pub struct EveError { + pub error: String, + pub time: DateTime, +} + +impl From for EveError { + fn from(error_and_time: ErrorAndTime) -> Self { + Self { + error: error_and_time.error_description.error, + time: error_and_time.error_description.error_time, + } + } +} + +#[derive(Debug)] +pub enum VaultStatus { + Unknown, + EncriptionDisabled(EveError, bool), + Unlocked(bool), + Locked(EveError, Option>), +} + pub type Model = RefCell; #[derive(Debug)] pub struct MonitorModel { pub dmesg: Vec, pub network: Vec, pub downloader: Option, + pub node_status: NodeStatus, + pub apps: HashMap, + pub app_summary: AppInstanceSummary, + pub vault_status: VaultStatus, +} + +impl From for VaultStatus { + fn from(vault_status: EveVaultStatus) -> Self { + let tpm_used = vault_status.pcr_status == PCRStatus::PcrEnabled; + match vault_status.status { + DataSecAtRestStatus::DataSecAtRestUnknown => Self::Unknown, + DataSecAtRestStatus::DataSecAtRestDisabled => { + let reason = EveError::from(vault_status.error_and_time); + Self::EncriptionDisabled(reason, tpm_used) + } + DataSecAtRestStatus::DataSecAtRestEnabled => Self::Unlocked(tpm_used), + DataSecAtRestStatus::DataSecAtRestError => { + let err = EveError::from(vault_status.error_and_time); + + let pcrs = if err.error.contains("Vault key unavailable") { + Some(vault_status.missmatching_pcrs) + } else { + None + }; + Self::Locked(err, pcrs) + } + } + } +} + +impl From for AppInstance { + fn from(app: AppInstanceStatus) -> Self { + let state = if !app + .error_and_time_with_source + .error_description + .error + .is_empty() + { + AppInstanceState::Error( + app.state, + app.error_and_time_with_source.error_description.error, + ) + } else { + AppInstanceState::Normal(app.state) + }; + + AppInstance { + name: app.display_name, + uuid: app.uuid_and_version.uuid, + version: app.uuid_and_version.version, + state, + } + } +} + +impl From for HashMap { + fn from(apps_list: AppsList) -> Self { + apps_list + .apps + .into_iter() + .map(|app| (app.uuid_and_version.uuid.clone(), AppInstance::from(app))) + .collect() + } +} + +impl From for NodeStatus { + fn from(node_status: EveNodeStatus) -> Self { + let onboarding_status = match (node_status.onboarded, node_status.node_uuid) { + (true, Some(uuid)) => OnboardingStatus::Onboarded(uuid), + (true, None) => OnboardingStatus::Error("Node UUID is missing".to_string()), + (false, _) => OnboardingStatus::Onboarding, + }; + NodeStatus { + server: node_status.server.clone(), + app_summary: node_status.app_instance_summary.unwrap_or_default(), + onboarding_status, + } + } } impl MonitorModel { - fn get_network_settings(&self, raw_model: &RawModel) -> Option> { - let network_status = raw_model.get_network_status()?; + fn get_network_settings( + &self, + network_status: DeviceNetworkStatus, + ) -> Option> { let ports = network_status.ports.as_ref()?; Some(ports.iter().map(|p| p.into()).collect()) } - pub fn update_from_raw_model(&mut self, raw_model: &RawModel) { - // we store only information enough to render the UI and - // the ID we can use to index the raw model e.g. networking port name - // FIXME: we can have a race condition when we get an updated raw model - // while we display a dialog with the old one - // we need to implement a way to handle this e.g. check update time or - // better do "almost equal" comparison algorithm - self.network = self.get_network_settings(raw_model).unwrap_or_default(); - self.downloader = raw_model.get_downloader_status().map(|f| (*f).clone()); + pub fn update_app_status(&mut self, state: AppInstanceStatus) { + let app_guid = &state.uuid_and_version.uuid; + self.apps + .entry(*app_guid) + .and_modify(|e| *e = AppInstance::from(state.clone())) + .or_insert(AppInstance::from(state)); + } + + pub fn update_app_list(&mut self, apps_list: AppsList) { + self.apps = HashMap::from(apps_list); + } + + pub fn update_downloader_status(&mut self, status: DownloaderStatus) { + self.downloader = Some(status); + } + + pub fn update_node_status(&mut self, status: EveNodeStatus) { + self.node_status = NodeStatus::from(status); + } + + pub fn update_app_summary(&mut self, app_summary: AppInstanceSummary) { + self.app_summary = app_summary; + } + + pub fn update_network_status(&mut self, net_status: DeviceNetworkStatus) { + self.network = self.get_network_settings(net_status).unwrap_or_default(); + } + + pub fn update_vault_status(&mut self, vault_status: EveVaultStatus) { + self.vault_status = VaultStatus::from(vault_status); + } + + pub fn update_onboarding_status(&mut self, status: EveOnboardingStatus) { + self.node_status.onboarding_status = OnboardingStatus::Onboarded(status.device_uuid); } } @@ -39,6 +201,10 @@ impl Default for MonitorModel { dmesg: Vec::with_capacity(1000), network: Vec::new(), downloader: None, + node_status: NodeStatus::default(), + apps: HashMap::new(), + app_summary: AppInstanceSummary::default(), + vault_status: VaultStatus::Unknown, } } } diff --git a/src/raw_model.rs b/src/raw_model.rs index ed6ce73..deabf6e 100644 --- a/src/raw_model.rs +++ b/src/raw_model.rs @@ -1,22 +1,16 @@ -use crate::ipc::eve_types::{ - DeviceNetworkStatus, DevicePortConfig, DevicePortConfigList, DownloaderStatus, - PhysicalIOAdapterList, -}; -#[derive(Debug)] +use uuid::Uuid; + +use crate::ipc::eve_types::{DeviceNetworkStatus, DevicePortConfig, DevicePortConfigList}; +#[derive(Debug, Default)] pub struct RawModel { dpc_list: Option, network_status: Option, - io_adapters: Option, - downloader_status: Option, } impl RawModel { pub fn new() -> Self { Self { - dpc_list: None, - network_status: None, - io_adapters: None, - downloader_status: None, + ..Default::default() } } @@ -24,34 +18,14 @@ impl RawModel { self.dpc_list = Some(dpc_list); } - pub fn set_network_status(&mut self, network_status: DeviceNetworkStatus) { - self.network_status = Some(network_status); - } - - pub fn set_io_adapters(&mut self, io_adapters: PhysicalIOAdapterList) { - self.io_adapters = Some(io_adapters); - } - - pub fn set_downloader_status(&mut self, downloader_status: DownloaderStatus) { - self.downloader_status = Some(downloader_status); - } - pub fn get_dpc_list(&self) -> Option<&DevicePortConfigList> { self.dpc_list.as_ref() } - pub fn get_network_status(&self) -> Option<&DeviceNetworkStatus> { + fn get_network_status(&self) -> Option<&DeviceNetworkStatus> { self.network_status.as_ref() } - pub fn get_io_adapters(&self) -> Option<&PhysicalIOAdapterList> { - self.io_adapters.as_ref() - } - - pub fn get_downloader_status(&self) -> Option<&DownloaderStatus> { - self.downloader_status.as_ref() - } - pub fn get_current_dpc(&self) -> Option<&DevicePortConfig> { let net_status = self.get_network_status()?; let key = &net_status.dpc_key; From 6c30fa6b1de8d4461ff97d8ef261eaffe5fae742 Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Sun, 1 Sep 2024 22:53:35 +0200 Subject: [PATCH 09/11] Add application tab Signed-off-by: Mikhail Malyshev --- src/ui/app_page.rs | 217 +++++++++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 1 + src/ui/ui.rs | 4 +- 3 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/ui/app_page.rs diff --git a/src/ui/app_page.rs b/src/ui/app_page.rs new file mode 100644 index 0000000..a91b360 --- /dev/null +++ b/src/ui/app_page.rs @@ -0,0 +1,217 @@ +use std::rc::Rc; + +use crossterm::event::{KeyCode, KeyModifiers}; +use ratatui::{ + layout::{Alignment, Constraint, Rect}, + style::{Color, Style, Stylize}, + text::{Text, ToText}, + widgets::{ + Block, BorderType, Borders, Cell, HighlightSpacing, Padding, Row, StatefulWidget, Table, + TableState, + }, + Frame, +}; + +use crate::{ + events::Event, + model::model::{AppInstance, AppInstanceState, Model}, + traits::{IEventHandler, IPresenter, IWidget, IWindow}, +}; + +use super::traits::ISelector; + +#[derive(Debug, Default)] +struct ApplicationList { + state: TableState, + size: usize, +} + +#[derive(Debug, Default)] +pub struct ApplicationsPage { + list: ApplicationList, +} + +impl ApplicationsPage { + pub fn new() -> Self { + ApplicationsPage { + ..Default::default() + } + } + fn render_app_list(&mut self, model: &Rc, list_rect: Rect, frame: &mut Frame) { + // create header for the table + let header = Row::new(vec![ + Cell::from("Name").style(Style::default()), + Cell::from("GUID").style(Style::default()), + Cell::from("Status").style(Style::default()), + ]); + + // create list items from the interface + let rows = model + .borrow() + .apps + .iter() + .map(|(_, app)| info_row_from_app(app)) + .collect::>(); + + self.list.size = rows.len(); + // self.interface_names = model + // .borrow() + // .network + // .iter() + // .map(|iface| iface.name.clone()) + // .collect(); + + // create a surrounding block for the list + let block = Block::default() + .title(" Applications ") + .title_alignment(Alignment::Center) + .borders(Borders::TOP) + .border_type(BorderType::Plain) + // .border_style(Style::default().fg(Color::White).bg(Color::Black)) + // .style(Style::default().bg(Color::Black)); + .padding(Padding::new(1, 1, 1, 1)); + + let bar = " █ "; + + // Create a List from all list items and highlight the currently selected one + let list = Table::new( + rows, + [ + Constraint::Max(20), + Constraint::Max(32), + Constraint::Fill(14), + ], + ) + .block(block) + .highlight_style(Style::new().bg(Color::DarkGray)) + // .highlight_symbol(">") + .highlight_symbol(Text::from(vec![ + // "".into(), + bar.into(), + bar.into(), + bar.into(), + bar.into(), + // "".into(), + ])) + .highlight_spacing(HighlightSpacing::Always) + .header(header); + + StatefulWidget::render(list, list_rect, frame.buffer_mut(), &mut self.list.state); + } +} + +impl IWindow for ApplicationsPage {} + +impl IEventHandler for ApplicationsPage { + fn handle_event(&mut self, event: Event) -> Option { + match event { + Event::Key(key) => match key.code { + KeyCode::Up => self.select_previous(), + KeyCode::Down => self.select_next(), + KeyCode::Home if key.modifiers == KeyModifiers::CONTROL => self.select_first(), + KeyCode::End if key.modifiers == KeyModifiers::CONTROL => self.select_last(), + KeyCode::Enter => { + let _selected_iface = self.selected(); + // if let Some(selected) = _selected_iface { + // return Some(Action::new("net", UiActions::EditIfaceConfig(selected))); + // } + } + _ => {} + }, + _ => {} + } + None + } +} + +fn info_row_from_app<'a, 'b>(app: &'a AppInstance) -> Row<'b> { + let height = 1; + // cells #1,2 IFace name and Link status + let mut cells = vec![ + Cell::from(app.name.clone()), + Cell::from(app.uuid.to_string()), + match &app.state { + AppInstanceState::Normal(st) => Cell::from(st.to_string()).style(Style::new().green()), + AppInstanceState::Error(st, _err) => { + Cell::from(st.to_string()).style(Style::new().red()) + } + }, + ]; + + // // collect IP addresses and add as multiline + // let ipv4_len = app.ipv4.as_ref().map_or(0, |v| v.len()); + // let ipv6_len = app.ipv6.as_ref().map_or(0, |v| v.len()); + + // let height = (ipv4_len + ipv6_len).max(1); + + // // join both ipv4 and ipv6 addresses and separate by newline + // let combined_ip_list_iter = app + // .ipv4 + // .iter() + // .chain(app.ipv6.iter()) + // .flat_map(|v| v.iter().cloned()) + // .map(|ip| ip.to_string()) + // .collect::>() + // .join("\n"); + + // // cell #3 IP address list + // if height > 1 { + // cells.push(Cell::from(combined_ip_list_iter).style(Style::new().white())); + // } else { + // cells.push(Cell::from("N/A").style(Style::new().red())); + // } + + // cell #4 MAC + //cells.push(Cell::from(app.mac.to_string()).style(Style::new().yellow())); + + Row::new(cells).height(height) +} + +impl IPresenter for ApplicationsPage { + fn render( + &mut self, + area: &ratatui::prelude::Rect, + frame: &mut ratatui::Frame<'_>, + model: &std::rc::Rc, + focused: bool, + ) { + self.render_app_list(model, *area, frame); + } +} + +impl ISelector for ApplicationsPage { + fn select_next(&mut self) { + if let Some(selected) = self.list.state.selected() { + if selected < self.list.size - 1 { + self.list.state.select(Some(selected + 1)); + } + } else { + self.list.state.select(Some(0)); + } + } + + fn select_previous(&mut self) { + if let Some(selected) = self.list.state.selected() { + let index = selected.saturating_sub(1); + self.list.state.select(Some(index)); + } + } + + fn select_first(&mut self) { + self.list.state.select(Some(0)); + } + + fn select_last(&mut self) { + let index = self.list.size.saturating_sub(1); + + self.list.state.select(Some(index)); + } + + fn selected(&self) -> Option { + // self.list + // .state + // .selected() + // .map(|index| self[index].clone()) + None + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9d25890..30f119b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -7,6 +7,7 @@ pub mod homepage; pub mod ipdialog; pub mod layer_stack; // pub mod netconf; +pub mod app_page; pub mod networkpage; pub mod statusbar; pub mod tools; diff --git a/src/ui/ui.rs b/src/ui/ui.rs index 86a96fb..87a5cbf 100644 --- a/src/ui/ui.rs +++ b/src/ui/ui.rs @@ -31,6 +31,7 @@ use crate::{ use super::{ action::Action, + app_page::ApplicationsPage, homepage::HomePage, layer_stack::LayerStack, networkpage::create_network_page, @@ -64,7 +65,7 @@ pub enum UiTabs { //Debug, Home, Network, - // Applications, + Applications, Dmesg, } @@ -226,6 +227,7 @@ impl Ui { self.views[UiTabs::Network as usize].push(Box::new(create_network_page())); + self.views[UiTabs::Applications as usize].push(Box::new(ApplicationsPage::new())); self.views[UiTabs::Dmesg as usize].push(Box::new(DmesgViewer::new())); } From 3e45149a58a9d73db90c9345a55f2581459088a8 Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Sun, 1 Sep 2024 22:57:08 +0200 Subject: [PATCH 10/11] Add Summary tab Signed-off-by: Mikhail Malyshev --- src/ui/mod.rs | 1 + src/ui/summary_page.rs | 258 +++++++++++++++++++++++++++++++++++++++++ src/ui/ui.rs | 3 + 3 files changed, 262 insertions(+) create mode 100644 src/ui/summary_page.rs diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 30f119b..8f62e65 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -10,6 +10,7 @@ pub mod layer_stack; pub mod app_page; pub mod networkpage; pub mod statusbar; +pub mod summary_page; pub mod tools; pub mod traits; pub mod ui; diff --git a/src/ui/summary_page.rs b/src/ui/summary_page.rs new file mode 100644 index 0000000..50efb17 --- /dev/null +++ b/src/ui/summary_page.rs @@ -0,0 +1,258 @@ +use std::rc::Rc; + +use log::info; +use ratatui::{ + layout::{Constraint, Layout}, + prelude::Rect, + style::{Color, Style}, + text::{Line, Span, Text}, + Frame, +}; + +use crate::{ + model::model::{Model, OnboardingStatus, VaultStatus}, + traits::{IEventHandler, IPresenter, IWindow}, +}; + +pub struct SummaryPage {} + +impl SummaryPage { + pub fn new() -> Self { + Self {} + } +} + +impl IWindow for SummaryPage {} + +impl IEventHandler for SummaryPage { + fn handle_event(&mut self, _event: crate::events::Event) -> Option { + None + } +} + +impl IPresenter for SummaryPage { + fn render(&mut self, area: &Rect, frame: &mut Frame<'_>, model: &Rc, focused: bool) { + let [server, onboarding_status_and_app_sunnary_rect, vault_status_rect] = + Layout::vertical(vec![ + Constraint::Length(3), + Constraint::Length(6), + Constraint::Fill(1), + ]) + .areas(*area); + + let [onboarding_status_rect, app_summary_rect] = + Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) + .areas(onboarding_status_and_app_sunnary_rect); + + let ns = &model.borrow().node_status; + info!("Node statue {:?}", ns); + let server_url = ratatui::widgets::Paragraph::new( + model + .borrow() + .node_status + .server + .clone() + .unwrap_or("N/A".to_string()), + ) + .block( + ratatui::widgets::Block::default() + .borders(ratatui::widgets::Borders::ALL) + .title("Server"), + ) + .style(ratatui::style::Style::default().fg(ratatui::style::Color::White)); + frame.render_widget(server_url, server); + + render_onboarding_status(model, frame, onboarding_status_rect); + render_app_summary(model, frame, app_summary_rect); + + render_vault_status(model, frame, vault_status_rect); + } +} + +fn render_onboarding_status( + model: &Rc, + frame: &mut Frame<'_>, + onboarding_status_rect: Rect, +) { + let onboarding_staus = model.borrow().node_status.onboarding_status.clone(); + let mut text = Vec::new(); + let mut spans = vec![]; + spans.push(Span::styled("status: ", Style::default().fg(Color::White))); + spans.push(match onboarding_staus { + OnboardingStatus::Unknown => Span::styled("Unknown", Style::default().fg(Color::Yellow)), + OnboardingStatus::Onboarding => { + Span::styled("Onboarding...", Style::default().fg(Color::Yellow)) + } + OnboardingStatus::Onboarded(_) => { + Span::styled("Onboarded", Style::default().fg(Color::Green)) + } + OnboardingStatus::Error(_) => Span::styled("Onboarded", Style::default().fg(Color::Red)), + }); + + text.push(Line::from(spans)); + + match onboarding_staus { + OnboardingStatus::Unknown => { + text.push(Line::from(vec![ + Span::styled("GUID: ", Style::default().fg(Color::White)), + Span::styled("N/A", Style::default().fg(Color::Yellow)), + ])); + text.push(Line::from(vec![ + Span::styled("Error: ", Style::default().fg(Color::White)), + Span::styled("N/A", Style::default().fg(Color::Green)), + ])); + } + OnboardingStatus::Onboarding => { + text.push(Line::from(vec![ + Span::styled("GUID: ", Style::default().fg(Color::White)), + Span::styled("N/A", Style::default().fg(Color::Yellow)), + ])); + text.push(Line::from(vec![ + Span::styled("Error: ", Style::default().fg(Color::White)), + Span::styled("N/A", Style::default().fg(Color::Green)), + ])); + } + OnboardingStatus::Onboarded(guid) => { + text.push(Line::from(vec![ + Span::styled("GUID: ", Style::default().fg(Color::White)), + Span::styled(format!("{}", guid), Style::default().fg(Color::White)), + ])); + text.push(Line::from(vec![ + Span::styled("Error: ", Style::default().fg(Color::White)), + Span::styled("N/A", Style::default().fg(Color::Green)), + ])); + } + OnboardingStatus::Error(err) => { + text.push(Line::from(vec![ + Span::styled("GUID: ", Style::default().fg(Color::White)), + Span::styled("N/A", Style::default().fg(Color::Yellow)), + ])); + text.push(Line::from(vec![ + Span::styled("Error: ", Style::default().fg(Color::White)), + Span::styled(err, Style::default().fg(Color::Red)), + ])); + } + } + + // let status = model.borrow().node_status.onboarding_status.clone(); + + let onboarding_status = ratatui::widgets::Paragraph::new(Text::from(text)) + .block( + ratatui::widgets::Block::default() + .borders(ratatui::widgets::Borders::ALL) + .title("Onboarding status"), + ) + .style(ratatui::style::Style::default().fg(ratatui::style::Color::White)); + frame.render_widget(onboarding_status, onboarding_status_rect); +} + +fn render_app_summary(model: &Rc, frame: &mut Frame<'_>, app_summary_rect: Rect) { + let apps = &model.borrow().node_status.app_summary; + + let mut app_summary_text = vec![]; + app_summary_text.push(Line::from(vec![ + Span::raw("Running: "), + Span::styled( + format!("{}", apps.total_running), + Style::default().fg(Color::Green), + ), + ])); + app_summary_text.push(Line::from(vec![ + Span::raw("Starting: "), + Span::styled( + format!("{}", apps.total_starting), + Style::default().fg(Color::Green), + ), + ])); + app_summary_text.push(Line::from(vec![ + Span::raw("Stopping: "), + Span::styled( + format!("{}", apps.total_stopping), + Style::default().fg(Color::Yellow), + ), + ])); + app_summary_text.push(Line::from(vec![ + Span::raw("In error: "), + Span::styled( + format!("{}", apps.total_error), + Style::default().fg(Color::Red), + ), + ])); + let app_summary = ratatui::widgets::Paragraph::new(Text::from(app_summary_text)) + .block( + ratatui::widgets::Block::default() + .borders(ratatui::widgets::Borders::ALL) + .title("App summary"), + ) + .style(ratatui::style::Style::default().fg(ratatui::style::Color::White)); + frame.render_widget(app_summary, app_summary_rect); +} + +fn render_vault_status(model: &Rc, frame: &mut Frame<'_>, onboarding_status_rect: Rect) { + let vault_status = &model.borrow().vault_status; + let mut text = Vec::new(); + let mut spans = vec![]; + spans.push(Span::styled("Status: ", Style::default().fg(Color::White))); + spans.push(match vault_status { + VaultStatus::Unknown => Span::styled("Unknown", Style::default().fg(Color::Yellow)), + VaultStatus::EncriptionDisabled(_, _) => { + Span::styled("Encription disabled", Style::default().fg(Color::Yellow)) + } + VaultStatus::Unlocked(_) => Span::styled("Unlocked", Style::default().fg(Color::Green)), + VaultStatus::Locked(_, _) => Span::styled("Locked", Style::default().fg(Color::Red)), + }); + + text.push(Line::from(spans)); + + match vault_status { + VaultStatus::Unknown => { + text.push(Line::from(vec![ + Span::styled("Error: ", Style::default().fg(Color::White)), + Span::styled("N/A", Style::default().fg(Color::Green)), + ])); + } + VaultStatus::EncriptionDisabled(reason, tpm_used) => { + text.push(Line::from(vec![ + Span::styled("TPM used: ", Style::default().fg(Color::White)), + if *tpm_used { + Span::styled("Yes", Style::default().fg(Color::Green)) + } else { + Span::styled("No", Style::default().fg(Color::Red)) + }, + ])); + text.push(Line::from(vec![ + Span::styled("Error: ", Style::default().fg(Color::Red)), + Span::styled(&reason.error, Style::default().fg(Color::White)), + ])); + } + VaultStatus::Unlocked(tpm_used) => { + text.push(Line::from(vec![ + Span::styled("Error: ", Style::default().fg(Color::White)), + Span::styled("N/A", Style::default().fg(Color::Green)), + ])); + text.push(Line::from(vec![ + Span::styled("TPM used: ", Style::default().fg(Color::White)), + if *tpm_used { + Span::styled("Yes", Style::default().fg(Color::Green)) + } else { + Span::styled("No", Style::default().fg(Color::Red)) + }, + ])); + } + VaultStatus::Locked(err, pcr) => { + text.push(Line::from(vec![ + Span::styled("Error: ", Style::default().fg(Color::Red)), + Span::styled(&err.error, Style::default().fg(Color::White)), + ])); + } + } + + let vault_status = ratatui::widgets::Paragraph::new(Text::from(text)) + .block( + ratatui::widgets::Block::default() + .borders(ratatui::widgets::Borders::ALL) + .title("Vault status"), + ) + .style(ratatui::style::Style::default().fg(ratatui::style::Color::White)); + frame.render_widget(vault_status, onboarding_status_rect); +} diff --git a/src/ui/ui.rs b/src/ui/ui.rs index 87a5cbf..08b581a 100644 --- a/src/ui/ui.rs +++ b/src/ui/ui.rs @@ -36,6 +36,7 @@ use super::{ layer_stack::LayerStack, networkpage::create_network_page, statusbar::{create_status_bar, StatusBarState}, + summary_page::SummaryPage, widgets::{ button::ButtonElement, input_field::{InputFieldElement, InputModifiers}, @@ -63,6 +64,7 @@ pub struct Ui { pub enum UiTabs { #[default] //Debug, + Summary, Home, Network, Applications, @@ -221,6 +223,7 @@ impl Ui { // MonActions::NetworkInterfaceUpdated(s), // ); + self.views[UiTabs::Summary as usize].push(Box::new(SummaryPage::new())); self.views[UiTabs::Home as usize].push(Box::new(HomePage::new())); // self.views[UiTabs::Home as usize].push(Box::new(d)); From 174cb10cab2c70f4427326e78f0cf7692818d9b2 Mon Sep 17 00:00:00 2001 From: Mikhail Malyshev Date: Sun, 1 Sep 2024 23:58:04 +0200 Subject: [PATCH 11/11] Fix AppSummay update and display affected PCRs Signed-off-by: Mikhail Malyshev --- src/application.rs | 2 -- src/ipc/eve_types.rs | 2 +- src/model/model.rs | 6 ++---- src/ui/summary_page.rs | 8 ++++++++ 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/application.rs b/src/application.rs index c377d31..288ed5c 100644 --- a/src/application.rs +++ b/src/application.rs @@ -348,8 +348,6 @@ impl Application { } pub async fn run(&mut self) -> Result<()> { - println!("Running application"); - let (ipc_task, ipc_cancellation_token, mut ipc_rx) = self.create_ipc_task(); // TODO: handle suspend/resume for the case when we give away /dev/tty diff --git a/src/ipc/eve_types.rs b/src/ipc/eve_types.rs index 04eeb35..9549150 100644 --- a/src/ipc/eve_types.rs +++ b/src/ipc/eve_types.rs @@ -1294,7 +1294,7 @@ pub struct EveVaultStatus { pub pcr_status: PCRStatus, pub conversion_complete: bool, #[serde(rename = "MissmatchingPCRs")] - pub missmatching_pcrs: Vec, + pub missmatching_pcrs: Option>, #[serde(flatten)] pub error_and_time: ErrorAndTime, // Unknown type, skipped } diff --git a/src/model/model.rs b/src/model/model.rs index bc6af4b..5c77a48 100644 --- a/src/model/model.rs +++ b/src/model/model.rs @@ -72,7 +72,6 @@ pub struct MonitorModel { pub downloader: Option, pub node_status: NodeStatus, pub apps: HashMap, - pub app_summary: AppInstanceSummary, pub vault_status: VaultStatus, } @@ -90,7 +89,7 @@ impl From for VaultStatus { let err = EveError::from(vault_status.error_and_time); let pcrs = if err.error.contains("Vault key unavailable") { - Some(vault_status.missmatching_pcrs) + vault_status.missmatching_pcrs } else { None }; @@ -179,7 +178,7 @@ impl MonitorModel { } pub fn update_app_summary(&mut self, app_summary: AppInstanceSummary) { - self.app_summary = app_summary; + self.node_status.app_summary = app_summary; } pub fn update_network_status(&mut self, net_status: DeviceNetworkStatus) { @@ -203,7 +202,6 @@ impl Default for MonitorModel { downloader: None, node_status: NodeStatus::default(), apps: HashMap::new(), - app_summary: AppInstanceSummary::default(), vault_status: VaultStatus::Unknown, } } diff --git a/src/ui/summary_page.rs b/src/ui/summary_page.rs index 50efb17..f3ece4c 100644 --- a/src/ui/summary_page.rs +++ b/src/ui/summary_page.rs @@ -244,6 +244,14 @@ fn render_vault_status(model: &Rc, frame: &mut Frame<'_>, onboarding_stat Span::styled("Error: ", Style::default().fg(Color::Red)), Span::styled(&err.error, Style::default().fg(Color::White)), ])); + text.push(Line::from(vec![ + Span::styled("Affected PCRs: ", Style::default().fg(Color::White)), + if let Some(pcr) = pcr { + Span::styled(format!("{:?}", pcr), Style::default().fg(Color::Green)) + } else { + Span::styled("N/A", Style::default().fg(Color::Yellow)) + }, + ])); } }