Skip to content

Conversation

ronny-standtke
Copy link
Member

This pull request introduces several updates across multiple files, focusing on adding network configuration management, enhancing rollback capabilities, and updating dependencies. The changes include new functionality for managing network settings, improvements to error handling, and updates to configuration files and dependencies.

Network Configuration Management:

  • src/api.rs: Added support for network configuration management, including functions to set network settings, handle server restarts with new certificates, and manage rollbacks for network changes. Introduced new types like NetworkSetting and PendingNetworkRollback, and added macros for file paths. [1] [2] [3]

  • src/main.rs: Added global server restart signal and rollback timer management to facilitate network rollback and server restart workflows.

Rollback Enhancements:

  • src/api.rs: Implemented rollback mechanisms for network settings, including saving, loading, and executing pending rollbacks, as well as canceling rollback timers.

  • src/main.rs: Added utility functions for managing rollback timers, such as set_rollback_timer and cancel_rollback_timer.

Dependency Updates:

  • Cargo.toml: Added new dependencies like rust-ini for INI file handling and actix-cors for cross-origin resource sharing. Updated existing dependency versions. [1] [2]

Configuration Updates:

  • biome.json: Upgraded schema version and added new linter rules for stricter code style enforcement. Updated file inclusion patterns. [1] [2] [3]

Miscellaneous Improvements:

  • src/certificate.rs: Modified create_module_certificate to accept an optional IP address for certificate generation, enabling dynamic updates based on network changes. [1] [2]

  • build-and-run-image.sh: Added volume mapping for the network directory to support network configuration changes in Docker containers.

Copy link
Contributor

@empwilli empwilli left a comment

Choose a reason for hiding this comment

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

LGTM overall minor nits

src/api.rs Outdated
Comment on lines 88 to 92
previous_ip: String,
ip: Option<String>,
netmask: Option<u8>,
gateway: Option<Vec<String>>,
dns: Option<Vec<String>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Preferably use strong types instead of "stringly types". The standard has your back here: std::net::IpAddr (+ std::net::Ipv4Addr, std::net::Ipv6Addr). serde conveniently already provides Serialize/Deserialize implementations for these types.

The benefit is, that you gain some addtional validity checks + convenience methods for working with these types.

src/api.rs Outdated
}

pub async fn set_network(
body: web::Json<NetworkSetting>,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: rather use "network_settings" than body. body communicates "this is the body of the request", whereas this already is the parsed body.

src/api.rs Outdated
Comment on lines 342 to 344
if body.name.is_empty() {
return HttpResponse::BadRequest().body("Network name cannot be empty");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it would be sensible to add in something like: https://crates.io/crates/serde_valid

src/api.rs Outdated
Comment on lines 345 to 372

if let Some(ip) = &body.ip {
if !ip.is_empty() && ip.parse::<std::net::IpAddr>().is_err() {
return HttpResponse::BadRequest().body("Invalid IP address format");
}
}

if let Some(netmask) = &body.netmask {
if *netmask > 32 {
return HttpResponse::BadRequest().body("Netmask must be between 0 and 32");
}
}

if let Some(gateway) = &body.gateway {
for gw in gateway {
if !gw.is_empty() && gw.parse::<std::net::IpAddr>().is_err() {
return HttpResponse::BadRequest().body("Invalid gateway format");
}
}
}

if let Some(dns) = &body.dns {
for dns_entry in dns {
if !dns_entry.is_empty() && dns_entry.parse::<std::net::IpAddr>().is_err() {
return HttpResponse::BadRequest().body("Invalid DNS format");
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The same goes for these checks

src/api.rs Outdated
Comment on lines 387 to 393
if fs::exists(&config_file)? {
fs::remove_file(&config_file)?;
}

if fs::exists(&backup_file)? {
fs::rename(backup_file, config_file)?;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Couple of things (but maybe I don't get the logic fully here):

  • I'd copy the backup file instead of moving it, to keep it around, just in case.
  • I'd remove the existing config file only if there is a backup file or do we have another level of fallback?
  • Probably it is sensible to make the operation atomic, i.e. copy backup to config file directly?

Copy link
Member Author

Choose a reason for hiding this comment

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

Another level of fallback would be the default network config in /lib/systemd/network/

Comment on lines 401 to 407
) -> Result<()> {
let _ = self.restore_network_setting(network.clone());
let _ = self.ods_client.reload_network().await;
let _ = certificate::create_module_certificate(Some(network.previous_ip)).await;
crate::trigger_server_restart();

Ok(())
Copy link
Contributor

Choose a reason for hiding this comment

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

Why have this function return result if the calls don't propagate/handle the errors?

src/api.rs Outdated
Comment on lines 414 to 420
if Path::new(&backup_file).exists() {
fs::remove_file(&backup_file).context("Unable to remove backup file")?;
}

if Path::new(&config_file).exists() {
fs::rename(config_file, backup_file).context("Failed to back up file")?;
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

File operations should always be performed atomically, ideally: try to do the operation that you want to perform and handle the unexpected case. In this case I'd do sth. like the following:

if let Err(ref e) = fs::remove_file("/tmp/some/file/path.txt") && 
    e.kind() != ErrorKind::NotFound {
    ...
}

the same goes for renaming/copying the backup file.

Copy link
Member Author

Choose a reason for hiding this comment

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

I do copy now instead of moving. So if it would fail to write new config, old config will be still available.

src/api.rs Outdated
Comment on lines 464 to 465
.as_secs()
+ 90; // 90 seconds from now
Copy link
Contributor

Choose a reason for hiding this comment

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

... + std::Duration::from_seconds(5) ... is longer but IMHO more expressive regarding the intend.

src/api.rs Outdated
#[derive(Serialize, Deserialize, Clone)]
struct PendingNetworkRollback {
network_setting: NetworkSetting,
rollback_time: u64,
Copy link
Contributor

Choose a reason for hiding this comment

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

If I understand the code correctly, the rollback_time is only used by the omnect-ui to check since when there is a network rollback in progress? serde provides serialization/deserialization for std::time::Duration, i.e., I think it would be best to make rollback_time a std::time::Duration and let serde handle the rest. This would simplify the code downstream as you don't have to manually convert via seconds from u64 to duration and back.

src/main.rs Outdated

pub fn trigger_server_restart() {
if let Some(tx) = SERVER_RESTART_TX.get() {
let _ = tx.send(());
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO we should log any errors for later analysis.

Copy link
Contributor

@alexomanie alexomanie left a comment

Choose a reason for hiding this comment

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

frontend seems ok for me :)

Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I understand the code there will never be a situation where we call trigger_server_restart or cancel_rollback_timer and they have not yet been initialized. Is there some test scenario that I overlook?

IMHO the if let Some(...) clauses are then complicated and confusing (as I have to wonder: what if they are not initialized? in what cases are they not initialized?). I'd probably go away from global variables and make a ServerControl struct that is part of API.

Both AbortHandle and broadcast::Sender implement Clone, i.e., their principle idea is, that instead of having a single instance that requires locking for synchornized access, they can be cloned cheaply and thus don't need synchronization (or, more correctly, take care of synchronization under the hood.).

src/api.rs Outdated
HttpResponse::Ok().finish()
}

fn restore_network_setting(&self, network: NetworkSetting) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a reason not to pass network by reference?

Copy link
Contributor

@empwilli empwilli left a comment

Choose a reason for hiding this comment

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

Minor nits, but looks good to me overall :)

src/api.rs Outdated
Ok(())
}

async fn configure_network_interface(&self, network: NetworkSetting) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

can we pass here network by reference?

src/api.rs Outdated

async fn handle_server_restart_with_new_certificate(
&self,
network: NetworkSetting,
Copy link
Contributor

Choose a reason for hiding this comment

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

pass network by reference

src/api.rs Outdated
}

pub async fn check_and_execute_pending_rollback(&self) -> Result<()> {
if let Some(pending) = self.load_pending_rollback() {
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO its fine to return early here to reduce nesting, i.e., let Some(pending) = self.load_pending_rollback() else return Ok(());

Copy link
Contributor

Choose a reason for hiding this comment

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

agree

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what the difference to the current implementation is supposed to be.

- Bumped versions for @vueuse/core, axios, centrifuge, vue, and vuetify in dependencies.
- Updated devDependencies: @biomejs/biome, @types/bun, @vitejs/plugin-vue, @vue/tsconfig, typescript, unocss, vite, vite-plugin-vuetify, and vue-tsc.
src/api.rs Outdated
error!("Failed to save pending rollback: {e:#}");
}

let network_clone = network.clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

please rename to let network = network.clone();

src/api.rs Outdated
}

pub async fn check_and_execute_pending_rollback(&self) -> Result<()> {
if let Some(pending) = self.load_pending_rollback() {
Copy link
Contributor

Choose a reason for hiding this comment

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

agree

src/api.rs Outdated
let config_file = network_path!(format!("10-{}.network", network.name));
let backup_file = network_path!(format!("10-{}.network.old", network.name));

if !fs::exists(&backup_file)? {
Copy link
Contributor

Choose a reason for hiding this comment

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

sure this is what you want? it might return with an error...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants