Skip to content

Commit b12ba17

Browse files
committed
feat: network config
Add network configuration management UI and backend: Backend: - Add network_config module with support for static IP and DHCP configuration - Implement network rollback mechanism with 90-second timeout - Add API endpoint for setting network configuration with validation - Refactor certificate management to use DeviceServiceClient reference - Move certificate creation to server startup after service client initialization - Remove certificate regeneration from network rollback flow - Add server restart channel for handling network changes - Cancel pending rollback on successful authentication Frontend: - Add Network page with network settings UI - Add NetworkSettings component for editing network configuration - Add NetworkActions component for network-related actions - Add useWaitForNewIp composable for handling IP changes - Update DeviceNetworks component to show network details - Refactor DeviceActions to use composable pattern - Update Vuetify and Biome configurations - Add route for /network page Dependencies: - Add rust-ini for network config file management - Add serde_valid for request validation - Add trait-variant for async trait support - Add actix-cors for CORS support - Bump version to 1.1.0 Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
1 parent 4211618 commit b12ba17

28 files changed

+1253
-454
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ license = "MIT OR Apache-2.0"
77
name = "omnect-ui"
88
readme = "README.md"
99
repository = "git@github.com:omnect/omnect-ui.git"
10-
version = "1.0.5"
10+
version = "1.1.0"
1111
build = "src/build.rs"
1212

1313
[dependencies]
14+
actix-cors = { version = "0.7", default-features = false }
1415
actix-files = { version = "0.6", default-features = false }
1516
actix-multipart = { version = "0.7", default-features = false, features = [
1617
"tempfile",
1718
"derive"
18-
]}
19-
actix-session = { version = "0.10", features = ["cookie-session"] }
19+
] }
2020
actix-server = { version = "2.6", default-features = false }
21+
actix-session = { version = "0.10", features = ["cookie-session"] }
2122
actix-web = { version = "4.11", default-features = false, features = [
2223
"macros",
2324
"rustls-0_23",
@@ -37,6 +38,7 @@ log-panics = { version = "2.1", default-features = false, features = [
3738
mockall = { version = "0.13", optional = true, default-features = false }
3839
rand_core = { version = "0.9", default-features = false, features = ["std"] }
3940
reqwest = { version = "0.12.23", default-features = false, features = ["json", "rustls-tls"] }
41+
rust-ini = { version = "0.21", default-features = false }
4042
rustls = { version = "0.23", default-features = false, features = [
4143
"aws_lc_rs",
4244
"std",
@@ -51,11 +53,13 @@ serde_json = { version = "1.0", default-features = false, features = [
5153
"raw_value",
5254
] }
5355
serde_repr = { version = "0.1", default-features = false }
56+
serde_valid = { version = "1.0", default-features = false }
5457
tokio = { version = "1.45", default-features = false, features = [
5558
"macros",
5659
"net",
5760
"process",
5861
] }
62+
trait-variant = { version = "0.1" }
5963
uuid = { version = "1.17", default-features = false, features = [
6064
"v4",
6165
] }
@@ -66,5 +70,5 @@ mock = ["dep:mockall"]
6670
[dev-dependencies]
6771
actix-http = "3.11"
6872
actix-service = "2.0"
69-
tempfile = "3.20"
7073
mockall_double = "0.3"
74+
tempfile = "3.20"

biome.json

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
33
"linter": {
44
"enabled": true,
55
"rules": {
@@ -10,23 +10,32 @@
1010
},
1111
"style": {
1212
"noParameterAssign": "off",
13-
"useImportType": "off"
13+
"useImportType": "off",
14+
"useAsConstAssertion": "error",
15+
"useDefaultParameterLast": "error",
16+
"useEnumInitializers": "error",
17+
"useSelfClosingElements": "error",
18+
"useSingleVarDeclarator": "error",
19+
"noUnusedTemplateLiteral": "error",
20+
"useNumberNamespace": "error",
21+
"noInferrableTypes": "error",
22+
"noUselessElse": "error"
1423
},
1524
"complexity": {
1625
"noStaticOnlyClass": "off",
1726
"useLiteralKeys": "off",
1827
"noForEach": "off"
1928
}
2029
},
21-
"ignore": []
30+
"includes": ["**"]
2231
},
2332
"formatter": {
2433
"enabled": true,
2534
"formatWithErrors": false,
2635
"indentStyle": "tab",
2736
"indentWidth": 2,
2837
"lineWidth": 150,
29-
"ignore": []
38+
"includes": ["**"]
3039
},
3140
"javascript": {
3241
"formatter": {
@@ -39,7 +48,7 @@
3948
}
4049
},
4150
"files": {
42-
"ignore": ["./.vscode*"],
51+
"includes": ["**", "!.vscode*"],
4352
"maxSize": 31457280
4453
}
4554
}

build-and-run-image.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ docker run --rm \
1818
-v $(pwd)/temp:/cert \
1919
-v /tmp:/socket \
2020
-v $(pwd)/temp/data:/data \
21+
-v $(pwd)/temp/network:/network \
2122
-u $(id -u):$(id -g) \
2223
-e RUST_LOG=debug \
2324
-e UI_PORT=1977 \

src/api.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::{
55
},
66
keycloak_client::SingleSignOnProvider,
77
middleware::TOKEN_EXPIRE_HOURS,
8+
network_config::{self, NetworkConfig},
89
omnect_device_service_client::{DeviceServiceClient, FactoryReset, LoadUpdate, RunUpdate},
910
};
1011
use actix_files::NamedFile;
@@ -18,6 +19,7 @@ use argon2::{
1819
};
1920
use log::{debug, error};
2021
use serde::Deserialize;
22+
use serde_valid::Validate;
2123
use std::sync::OnceLock;
2224
use std::{
2325
fs::{self, File},
@@ -161,6 +163,7 @@ where
161163
pub async fn token(session: Session) -> impl Responder {
162164
debug!("token() called");
163165

166+
network_config::cancel_rollback();
164167
Self::session_token(session)
165168
}
166169

@@ -289,6 +292,30 @@ where
289292
HttpResponse::Ok().finish()
290293
}
291294

295+
pub async fn set_network_config(
296+
network_config: web::Json<NetworkConfig>,
297+
api: web::Data<Self>,
298+
) -> impl Responder {
299+
debug!("set_network_config() called");
300+
301+
if let Err(e) = network_config.validate() {
302+
error!("set_network_config() failed: {e:#}");
303+
return HttpResponse::BadRequest().body(format!("{e:#}"));
304+
}
305+
306+
if let Err(e) =
307+
network_config::apply_network_config(&api.service_client, &network_config).await
308+
{
309+
error!("set_network_config() failed: {e:#}");
310+
if let Err(err) = network_config::rollback_network_config(&network_config) {
311+
error!("Failed to restore network config: {err:#}");
312+
}
313+
return HttpResponse::InternalServerError().body(format!("{e:#}"));
314+
}
315+
316+
HttpResponse::Ok().finish()
317+
}
318+
292319
async fn validate_token_and_claims(&self, token: &str) -> Result<()> {
293320
let claims = self.single_sign_on.verify_token(token).await?;
294321
let Some(tenant_list) = &claims.tenant_list else {

src/certificate.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use crate::{
44
common::handle_http_response,
55
http_client::HttpClientFactory,
6-
omnect_device_service_client::{DeviceServiceClient, OmnectDeviceServiceClient},
6+
omnect_device_service_client::{DeviceServiceClient},
77
};
88
use anyhow::{Context, Result};
99
use log::info;
@@ -47,9 +47,12 @@ pub async fn create_module_certificate() -> Result<()> {
4747
}
4848

4949
#[cfg(not(feature = "mock"))]
50-
pub async fn create_module_certificate() -> Result<()> {
50+
pub async fn create_module_certificate<T>(service_client: &T) -> Result<()>
51+
where
52+
T: DeviceServiceClient,
53+
{
5154
info!("create module certificate");
52-
let ods_client = OmnectDeviceServiceClient::new(false).await?;
55+
5356
let id = std::env::var("IOTEDGE_MODULEID")
5457
.context("failed to read IOTEDGE_MODULEID environment variable")?;
5558
let gen_id = std::env::var("IOTEDGE_MODULEGENERATIONID")
@@ -60,7 +63,7 @@ pub async fn create_module_certificate() -> Result<()> {
6063
.context("failed to read IOTEDGE_WORKLOADURI environment variable")?;
6164

6265
let payload = CreateCertPayload {
63-
common_name: ods_client.ip_address().await?,
66+
common_name: service_client.ip_address().await?,
6467
};
6568

6669
let path = format!("/modules/{id}/genid/{gen_id}/certificate/server?api-version={api_version}");

src/keycloak_client.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use jwt_simple::prelude::{RS256PublicKey, RSAPublicKeyLike};
55
#[cfg(feature = "mock")]
66
use mockall::automock;
77
use serde::{Deserialize, Serialize};
8+
use trait_variant::make;
89

910
#[derive(Debug, Deserialize, Serialize, Clone)]
1011
pub struct TokenClaims {
@@ -26,9 +27,9 @@ macro_rules! keycloak_url {
2627
}};
2728
}
2829

30+
#[make(Send + Sync)]
2931
#[cfg_attr(feature = "mock", automock)]
30-
#[allow(async_fn_in_trait)]
31-
pub trait SingleSignOnProvider: Send + Sync {
32+
pub trait SingleSignOnProvider {
3233
async fn verify_token(&self, token: &str) -> anyhow::Result<TokenClaims>;
3334
}
3435

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
pub mod api;
22
pub mod auth;
3+
pub mod certificate;
34
pub mod common;
45
pub mod http_client;
56
pub mod keycloak_client;
67
pub mod middleware;
8+
pub mod network_config;
79
pub mod omnect_device_service_client;

src/main.rs

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ mod common;
55
mod http_client;
66
mod keycloak_client;
77
mod middleware;
8+
mod network_config;
89
mod omnect_device_service_client;
910

1011
use crate::{
1112
api::Api,
1213
certificate::create_module_certificate,
1314
common::{centrifugo_config, config_path},
1415
keycloak_client::KeycloakProvider,
16+
network_config::init_server_restart_channel,
1517
omnect_device_service_client::{DeviceServiceClient, OmnectDeviceServiceClient},
1618
};
19+
use actix_cors::Cors;
1720
use actix_files::Files;
1821
use actix_multipart::form::MultipartFormConfig;
1922
use actix_server::ServerHandle;
@@ -35,6 +38,7 @@ use std::{fs, io::Write};
3538
use tokio::{
3639
process::{Child, Command},
3740
signal::unix::{SignalKind, signal},
41+
sync::broadcast,
3842
};
3943

4044
const UPLOAD_LIMIT_BYTES: usize = 250 * 1024 * 1024;
@@ -68,31 +72,59 @@ async fn main() {
6872
env!("GIT_SHORT_REV")
6973
);
7074

71-
create_module_certificate()
72-
.await
73-
.expect("failed to create module certificate");
75+
// Create restart signal channel
76+
let (restart_tx, mut restart_rx) = broadcast::channel(1);
77+
init_server_restart_channel(restart_tx).expect("failed to set restart channel");
7478

7579
let mut sigterm = signal(SignalKind::terminate()).expect("failed to install SIGTERM handler");
80+
81+
CryptoProvider::install_default(default_provider()).expect("failed to install crypto provider");
82+
83+
while run_until_shutdown(&mut restart_rx, &mut sigterm).await {
84+
info!("restarting server...");
85+
}
86+
87+
debug!("good bye");
88+
}
89+
90+
async fn run_until_shutdown(
91+
restart_rx: &mut broadcast::Receiver<()>,
92+
sigterm: &mut tokio::signal::unix::Signal,
93+
) -> bool {
7694
let mut centrifugo = run_centrifugo();
7795
let (server_handle, server_task, service_client) = run_server().await;
7896

79-
tokio::select! {
97+
let should_restart = tokio::select! {
8098
_ = tokio::signal::ctrl_c() => {
8199
debug!("ctrl-c received");
100+
false
82101
},
83102
_ = sigterm.recv() => {
84103
debug!("SIGTERM received");
104+
false
105+
},
106+
_ = restart_rx.recv() => {
107+
debug!("server restart requested");
108+
true
85109
},
86-
_ = server_task => {
87-
debug!("server stopped unexpectedly");
110+
result = server_task => {
111+
match result {
112+
Ok(Ok(())) => debug!("server stopped normally"),
113+
Ok(Err(e)) => debug!("server stopped with error: {e}"),
114+
Err(e) => debug!("server task panicked: {e}"),
115+
}
116+
false
88117
},
89118
_ = centrifugo.wait() => {
90119
debug!("centrifugo stopped unexpectedly");
120+
false
91121
}
92-
}
122+
};
93123

94124
// Unified cleanup sequence - ensures consistent shutdown regardless of exit reason
95-
info!("shutting down...");
125+
if !should_restart {
126+
info!("shutting down...");
127+
}
96128

97129
// 1. Shutdown service client first (unregister from omnect-device-service)
98130
if let Err(e) = service_client.shutdown().await {
@@ -101,22 +133,26 @@ async fn main() {
101133

102134
// 2. Stop the server gracefully
103135
server_handle.stop(true).await;
104-
info!("server stopped");
136+
if !should_restart {
137+
info!("server stopped");
138+
}
105139

106140
// 3. Kill centrifugo
107141
if let Err(e) = centrifugo.kill().await {
108142
error!("failed to kill centrifugo: {e:#}");
109143
}
110-
info!("centrifugo stopped");
144+
if !should_restart {
145+
info!("centrifugo stopped");
146+
}
147+
148+
should_restart
111149
}
112150

113151
async fn run_server() -> (
114152
ServerHandle,
115153
tokio::task::JoinHandle<Result<(), std::io::Error>>,
116154
OmnectDeviceServiceClient,
117155
) {
118-
CryptoProvider::install_default(default_provider()).expect("failed to install crypto provider");
119-
120156
let Ok(true) = fs::exists("/data") else {
121157
panic!("failed to find required data directory: /data is missing");
122158
};
@@ -137,6 +173,19 @@ async fn run_server() -> (
137173
.await
138174
.expect("failed to create api");
139175

176+
create_module_certificate(&service_client)
177+
.await
178+
.expect("failed to create module certificate");
179+
180+
tokio::spawn({
181+
let service_client = service_client.clone();
182+
async move {
183+
if let Err(e) = network_config::process_pending_rollback(&service_client).await {
184+
error!("failed to check pending rollback: {e:#}");
185+
}
186+
}
187+
});
188+
140189
let mut tls_certs = std::io::BufReader::new(
141190
std::fs::File::open(certificate::cert_path()).expect("failed to read certificate file"),
142191
);
@@ -173,6 +222,14 @@ async fn run_server() -> (
173222

174223
let server = HttpServer::new(move || {
175224
App::new()
225+
.wrap(
226+
Cors::default()
227+
.allow_any_origin()
228+
.allow_any_header()
229+
.allowed_methods(vec!["GET"])
230+
.supports_credentials()
231+
.max_age(3600),
232+
)
176233
.wrap(
177234
SessionMiddleware::builder(CookieSessionStore::default(), session_key.clone())
178235
.cookie_name(String::from("omnect-ui-session"))
@@ -240,6 +297,7 @@ async fn run_server() -> (
240297
.route("/version", web::get().to(UiApi::version))
241298
.route("/logout", web::post().to(UiApi::logout))
242299
.route("/healthcheck", web::get().to(UiApi::healthcheck))
300+
.route("/network", web::post().to(UiApi::set_network_config))
243301
.service(Files::new(
244302
"/static",
245303
std::fs::canonicalize("static").expect("failed to find static folder"),

0 commit comments

Comments
 (0)