Skip to content

Commit

Permalink
Merge 9af9504 into f8b7d4b
Browse files Browse the repository at this point in the history
  • Loading branch information
otavio committed Jun 5, 2021
2 parents f8b7d4b + 9af9504 commit 891e678
Show file tree
Hide file tree
Showing 8 changed files with 559 additions and 27 deletions.
4 changes: 4 additions & 0 deletions updatehub/src/firmware/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ pub(crate) fn state_change_hook(path: &Path) -> PathBuf {
path.join(STATE_CHANGE_CALLBACK)
}

pub(crate) fn validate_hook(path: &Path) -> PathBuf {
path.join(VALIDATE_CALLBACK)
}

pub(crate) fn device_identity_dir(path: &Path) -> PathBuf {
path.join(DEVICE_IDENTITY_DIR).join("identity")
}
Expand Down
192 changes: 169 additions & 23 deletions updatehub/src/runtime_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,43 @@ pub enum Error {

#[display(fmt = "invalid runtime settings destination")]
InvalidDestination,

#[cfg(feature = "v1-parsing")]
#[display(fmt = "parsing error: json: {}, ini: {}", _0, _1)]
V1Parsing(serde_json::Error, serde_ini::de::Error),
}

#[derive(Clone, Debug, Deref, DerefMut, PartialEq)]
pub struct RuntimeSettings(pub api::RuntimeSettings);
pub struct RuntimeSettings {
#[deref]
#[deref_mut]
pub inner: api::RuntimeSettings,

#[cfg(feature = "v1-parsing")]
v1_content: Option<String>,
}

impl Default for RuntimeSettings {
fn default() -> Self {
RuntimeSettings(api::RuntimeSettings {
polling: api::RuntimePolling {
last: DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc),
retries: 0,
now: false,
server_address: api::ServerAddress::Default,
RuntimeSettings {
inner: api::RuntimeSettings {
polling: api::RuntimePolling {
last: DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc),
retries: 0,
now: false,
server_address: api::ServerAddress::Default,
},
update: api::RuntimeUpdate {
upgrade_to_installation: None,
applied_package_uid: None,
},
path: std::path::PathBuf::new(),
persistent: false,
},
update: api::RuntimeUpdate { upgrade_to_installation: None, applied_package_uid: None },
path: std::path::PathBuf::new(),
persistent: false,
})

#[cfg(feature = "v1-parsing")]
v1_content: None,
}
}
}

Expand Down Expand Up @@ -72,7 +91,19 @@ impl RuntimeSettings {
}

fn parse(content: &str) -> Result<Self> {
Ok(RuntimeSettings(serde_json::from_str::<api::RuntimeSettings>(content)?))
let runtime_settings = serde_json::from_str(content).map(|s| RuntimeSettings {
inner: s,
#[cfg(feature = "v1-parsing")]
v1_content: None,
});

#[cfg(feature = "v1-parsing")]
let runtime_settings = runtime_settings.or_else(|e| {
v1_parse(content, e)
.map(|s| RuntimeSettings { inner: s, v1_content: Some(content.to_string()) })
});

runtime_settings.map_err(Error::from)
}

fn save(&self) -> Result<()> {
Expand All @@ -94,7 +125,7 @@ impl RuntimeSettings {
}

fn serialize(&self) -> Result<String> {
Ok(serde_json::to_string(&self.0)?)
Ok(serde_json::to_string(&self.inner)?)
}

pub(crate) fn get_inactive_installation_set(&self) -> Result<Set> {
Expand Down Expand Up @@ -188,6 +219,72 @@ impl RuntimeSettings {

self.save()
}

#[cfg(feature = "v1-parsing")]
pub(crate) fn restore_v1_content(&mut self) -> Result<()> {
// Restore the original content of the file to not break the rollback
// procedure when rebooting.
if let Some(content) = &self.v1_content {
warn!("restoring previous content of runtime settings for v1 compatibility");
fs::write(&self.path, content)?;
}

Ok(())
}
}

#[cfg(feature = "v1-parsing")]
fn v1_parse(content: &str, json_err: serde_json::Error) -> Result<api::RuntimeSettings> {
use crate::utils::deserialize;
use serde::Deserialize;

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "PascalCase")]
struct RuntimeSettings {
polling: RuntimePolling,
update: RuntimeUpdate,
}

#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct RuntimePolling {
last_poll: DateTime<Utc>,
retries: usize,
#[serde(rename = "ProbeASAP")]
#[serde(deserialize_with = "deserialize::boolean")]
probe_asap: bool,
}

#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct RuntimeUpdate {
pub upgrade_to_installation: i8,
}

let old_runtime_settings = serde_ini::de::from_str::<RuntimeSettings>(content)
.map_err(|ini_err| Error::V1Parsing(json_err, ini_err))?;

warn!("loaded v1 runtime settings successfully");

Ok(api::RuntimeSettings {
polling: api::RuntimePolling {
last: old_runtime_settings.polling.last_poll,
retries: old_runtime_settings.polling.retries,
now: old_runtime_settings.polling.probe_asap,
server_address: api::ServerAddress::Default,
},
update: api::RuntimeUpdate {
upgrade_to_installation: match old_runtime_settings.update.upgrade_to_installation {
0 => Some(api::InstallationSet::A),
1 => Some(api::InstallationSet::B),
_ => None,
},
applied_package_uid: None,
},
path: std::path::PathBuf::new(),
persistent: false,
})
}

#[cfg(test)]
Expand All @@ -198,17 +295,25 @@ mod tests {
#[test]
fn default() {
let settings = RuntimeSettings::default();
let expected = RuntimeSettings(api::RuntimeSettings {
polling: api::RuntimePolling {
last: DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc),
retries: 0,
now: false,
server_address: api::ServerAddress::Default,
let expected = RuntimeSettings {
inner: api::RuntimeSettings {
polling: api::RuntimePolling {
last: DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc),
retries: 0,
now: false,
server_address: api::ServerAddress::Default,
},
update: api::RuntimeUpdate {
upgrade_to_installation: None,
applied_package_uid: None,
},
path: std::path::PathBuf::new(),
persistent: false,
},
update: api::RuntimeUpdate { upgrade_to_installation: None, applied_package_uid: None },
path: std::path::PathBuf::new(),
persistent: false,
});

#[cfg(feature = "v1-parsing")]
v1_content: None,
};

assert_eq!(Some(settings), Some(expected));
}
Expand Down Expand Up @@ -253,4 +358,45 @@ mod tests {
);
fs::remove_file(old_file).unwrap();
}

#[cfg(feature = "v1-parsing")]
#[test]
fn v1_parsing() {
let sample = r"
[Polling]
LastPoll=2021-06-01T14:38:57-03:00
FirstPoll=2021-05-01T13:33:33-03:00
ExtraInterval=0
Retries=0
ProbeASAP=false
[Update]
UpgradeToInstallation=1
";

let expected = RuntimeSettings {
inner: api::RuntimeSettings {
polling: api::RuntimePolling {
last: DateTime::from_utc(
DateTime::parse_from_rfc3339("2021-06-01T14:38:57-03:00")
.unwrap()
.naive_utc(),
Utc,
),
retries: 0,
now: false,
server_address: api::ServerAddress::Default,
},
update: api::RuntimeUpdate {
upgrade_to_installation: Some(api::InstallationSet::B),
applied_package_uid: None,
},
path: std::path::PathBuf::new(),
persistent: false,
},
v1_content: Some(sample.to_string()),
};

assert_eq!(RuntimeSettings::parse(sample).unwrap(), expected);
}
}
2 changes: 1 addition & 1 deletion updatehub/src/states/machine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub(super) trait CommunicationState: StateChangeImpl {
version: crate::version().to_string(),
config: context.settings.0.clone(),
firmware: context.firmware.0.clone(),
runtime_settings: context.runtime_settings.0.clone(),
runtime_settings: context.runtime_settings.inner.clone(),
}),
None,
))
Expand Down
8 changes: 8 additions & 0 deletions updatehub/src/states/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,14 @@ fn handle_startup_callbacks(
warn!("swapped active installation set and running rollback");
firmware::rollback_callback(&settings.firmware.metadata)?;
runtime_settings.reset_installation_settings()?;

// In case we are booting from an UpdateHub v1 update and
// the validation has failed, we need to restore the
// original content of the file to not break the rollback
// procedure when rebooting.
#[cfg(feature = "v1-parsing")]
runtime_settings.restore_v1_content()?;

easy_process::run("reboot")?;
}
Transition::Continue => firmware::installation_set::validate()?,
Expand Down
39 changes: 39 additions & 0 deletions updatehub/src/states/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,42 @@ fn startup_on_wrong_install_set() {
Ok(content) => panic!("Output file should be empty, instead we have: {}", content),
}
}

#[test]
#[cfg(feature = "v1-parsing")]
fn validate_v1_restored_runtime_settings() {
let setup = crate::tests::TestEnvironment::build().add_echo_binary("reboot").finish();
let output_file_path = &setup.binaries.data;
// Setup validation callback to always fail
fs::write(
setup.firmware.stored_path.join("validate-callback"),
format!("#!/bin/sh\necho $0 >> {}\nexit 1", output_file_path.to_string_lossy()),
)
.unwrap();
// Overwrite runtimesettings with a v1 model
let original_runtime_settings = r#"
[Polling]
LastPoll=2021-06-01T14:38:57-03:00
FirstPoll=2021-05-01T13:33:33-03:00
ExtraInterval=0
Retries=0
ProbeASAP=false
[Update]
UpgradeToInstallation=0
"#;
std::fs::write(&setup.runtime_settings.stored_path, &original_runtime_settings).unwrap();
let mut loaded_runtime_settings =
RuntimeSettings::load(&setup.runtime_settings.stored_path).unwrap();

// Remove the file to make sure the function will recreate it
std::fs::remove_file(&setup.runtime_settings.stored_path).unwrap();

handle_startup_callbacks(&setup.settings.data, &mut loaded_runtime_settings).unwrap();

assert_eq!(
std::fs::read_to_string(&setup.runtime_settings.stored_path).unwrap(),
original_runtime_settings,
"Reverted runtime settings did not match original v1 file"
);
}
30 changes: 27 additions & 3 deletions updatehub/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
//
// SPDX-License-Identifier: Apache-2.0

use crate::firmware::tests::{
create_fake_installation_set, create_fake_starup_callbacks, create_hook, device_attributes_dir,
device_identity_dir, hardware_hook, product_uid_hook, state_change_hook, version_hook,
use crate::firmware::{
installation_set::Set,
tests::{
create_fake_installation_set, create_fake_starup_callbacks, create_hook,
device_attributes_dir, device_identity_dir, hardware_hook, product_uid_hook,
state_change_hook, validate_hook, version_hook,
},
};
use sdk::api::info::runtime_settings::InstallationSet;
use std::{any::Any, env, fs, io::Write, os::unix::fs::PermissionsExt, path::PathBuf};

pub use crate::{
Expand Down Expand Up @@ -36,6 +41,8 @@ pub struct TestEnvironmentBuilder {
listen_socket: Option<String>,
supported_install_modes: Option<Vec<&'static str>>,
state_change_callback: Option<String>,
validate_callback: Option<String>,
booting_from_update: bool,
}

impl TestEnvironment {
Expand Down Expand Up @@ -82,6 +89,14 @@ impl TestEnvironmentBuilder {
TestEnvironmentBuilder { state_change_callback: Some(script), ..self }
}

pub fn validate_callback(self, script: String) -> Self {
TestEnvironmentBuilder { validate_callback: Some(script), ..self }
}

pub fn booting_from_update(self) -> Self {
TestEnvironmentBuilder { booting_from_update: true, ..self }
}

#[allow(clippy::field_reassign_with_default)]
pub fn finish(self) -> TestEnvironment {
let firmware = {
Expand Down Expand Up @@ -134,6 +149,11 @@ impl TestEnvironmentBuilder {
// Startup callbacks will be stored in the firmware directory
create_fake_starup_callbacks(&firmware.stored_path, &output_file);

if let Some(script) = self.validate_callback {
// Overwrite the validate callback
create_hook(validate_hook(&firmware.stored_path), &script);
}

for bin in self.extra_binaries.into_iter() {
let mut file = fs::File::create(&bin_dir_path.join(&bin)).unwrap();
writeln!(file, "#!/bin/sh\necho {} $@ >> {}", bin, output_file.to_string_lossy())
Expand All @@ -159,6 +179,10 @@ impl TestEnvironmentBuilder {

let mut runtime_settings = RuntimeSettings::default();
runtime_settings.path = file_path.clone();
if self.booting_from_update {
runtime_settings.enable_persistency();
runtime_settings.set_upgrading_to(Set(InstallationSet::A)).unwrap();
}

Data { data: runtime_settings, stored_path: file_path, guard: vec![Box::new(file)] }
};
Expand Down
Loading

0 comments on commit 891e678

Please sign in to comment.