diff --git a/src/common/loop_device.rs b/src/common/loop_device.rs index 096e4cf..7585e06 100644 --- a/src/common/loop_device.rs +++ b/src/common/loop_device.rs @@ -157,7 +157,7 @@ impl LoopDevice { } } } - + #[allow(dead_code)] /// create a loop device associated with the given file pub fn for_file>( file: P, diff --git a/src/stage1/api_calls.rs b/src/stage1/api_calls.rs index afb9a05..87d08a1 100644 --- a/src/stage1/api_calls.rs +++ b/src/stage1/api_calls.rs @@ -23,6 +23,8 @@ struct ImageRequestData { version: String, #[serde(rename = "fileType")] file_type: String, + #[serde(rename = "imageType")] + image_type: Option, } /// Structs corresponding to API response for endpoint /v6/releases #[derive(Serialize, Deserialize, Debug)] @@ -46,6 +48,39 @@ struct DeviceIdEntry { id: u32, } +/// Structs corresponding to API response for DeviceType Contract +#[derive(Debug, Deserialize)] +struct ContractData { + media: Media, + #[serde(default)] + #[serde(rename = "flashProtocol")] + flash_protocol: Option, +} + +#[derive(Debug, Deserialize)] +struct Media { + #[serde(default)] + #[serde(rename = "altBoot")] + alt_boot: Option>, + #[serde(rename = "defaultBoot")] + default_boot: String, +} + +#[derive(Debug, Deserialize)] +struct Contract { + data: ContractData, +} + +#[derive(Debug, Deserialize)] +struct DeviceTypeContractInfo { + contract: Contract, +} + +#[derive(Debug, Deserialize)] +struct DeviceContractInfoApiResponse { + d: Vec, +} + pub(crate) fn get_os_versions(api_endpoint: &str, api_key: &str, device: &str) -> Result { let mut headers = header::HeaderMap::new(); headers.insert( @@ -121,10 +156,21 @@ pub(crate) fn get_os_image( let request_url = format!("{}{}", api_endpoint, OS_IMG_URL); - let post_data = ImageRequestData { - device_type: String::from(device), - version: String::from(version), - file_type: String::from(".gz"), + let post_data = if is_device_image_flasher(api_endpoint, api_key, device)? { + debug!("Downloading raw image for device type {device}"); + ImageRequestData { + device_type: String::from(device), + version: String::from(version), + file_type: String::from(".gz"), + image_type: Some(String::from("raw")), + } + } else { + ImageRequestData { + device_type: String::from(device), + version: String::from(version), + file_type: String::from(".gz"), + image_type: None, + } }; debug!("get_os_image: request_url: '{}'", request_url); @@ -161,7 +207,7 @@ pub(crate) fn patch_device_type( ); // Before we can patch the deviceType, we need to get the deviceId corresponding to the slug - let dt_id_request_url = format!("{api_endpoint}{DEVICE__TYPE_URL_ENDPOINT}?$orderby=name%20asc&$top=1&$select=id&$filter=device_type_alias/any(dta:dta/is_referenced_by__alias%20eq%20%27{dt_slug}%27)"); + let dt_id_request_url = get_device_type_info_url(api_endpoint, "id", dt_slug); debug!( "patch_device_type: dt_id_request_url: '{}'", @@ -242,3 +288,65 @@ pub(crate) fn patch_device_type( )) } } + +fn is_device_image_flasher(api_endpoint: &str, api_key: &str, device: &str) -> Result { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::AUTHORIZATION, + header::HeaderValue::from_str(format!("Bearer {api_key}").as_str()) + .upstream_with_context("Failed to create auth header")?, + ); + let dt_contract_request_url = get_device_type_info_url(api_endpoint, "contract", device); + let res = Client::builder() + .default_headers(headers.clone()) + .build() + .upstream_with_context("Failed to create https client")? + .get(&dt_contract_request_url) + .send() + .upstream_with_context(&format!( + "Failed to send https request url: '{}'", + dt_contract_request_url + ))?; + + debug!("dt_contract_request Result = {:?}", res); + + let status = res.status(); + if status.is_success() { + let parsed_contract_resp = res + .json::() + .upstream_with_context("Failed to parse request results")?; + + // determine if device type's OS image is of flasher type + // ref: https://github.com/balena-io/contracts/blob/d06ad25196f67c4d20ad309941192fdddf80e307/README.md?plain=1#L81 + let device_contract = &parsed_contract_resp.d[0]; + debug!("Device contract for {device} is {:?}", device_contract); + + // If the defaultBoot is internal and there is an alternative boot method like sdcard and no flashProtocol defined -> flasher + if device_contract.contract.data.media.default_boot == "internal" + && device_contract + .contract + .data + .media + .alt_boot + .as_ref() + .is_some_and(|alt_boot_vec| !alt_boot_vec.is_empty()) + && device_contract.contract.data.flash_protocol.is_none() + { + Ok(true) + } else { + Ok(false) + } + } else { + Err(Error::with_context( + ErrorKind::InvState, + &format!( + "Balena API GET Device Type contract request failed with status: {}", + status + ), + )) + } +} + +fn get_device_type_info_url(api_endpoint: &str, select: &str, device: &str) -> String { + format!("{api_endpoint}{DEVICE__TYPE_URL_ENDPOINT}?$orderby=name%20asc&$top=1&$select={select}&$filter=device_type_alias/any(dta:dta/is_referenced_by__alias%20eq%20%27{device}%27)") +} diff --git a/src/stage1/image_retrieval.rs b/src/stage1/image_retrieval.rs index 931be2a..e96a090 100644 --- a/src/stage1/image_retrieval.rs +++ b/src/stage1/image_retrieval.rs @@ -1,5 +1,5 @@ -use std::fs::{self, create_dir, remove_dir, File, OpenOptions}; -use std::io::{copy, Read}; +use std::fs::File; +use std::io::copy; use std::path::{Path, PathBuf}; use log::{debug, error, info, warn, Level}; @@ -7,15 +7,7 @@ use log::{debug, error, info, warn, Level}; use semver::{Version, VersionReq}; use crate::{ - common::{ - defs::NIX_NONE, - disk_util::{Disk, PartitionIterator, PartitionReader}, - is_admin, - loop_device::LoopDevice, - path_append, - stream_progress::StreamProgress, - Error, Options, Result, ToError, - }, + common::{path_append, stream_progress::StreamProgress, Error, Options, Result, ToError}, stage1::{ api_calls::{get_os_image, get_os_versions, Versions}, defs::{ @@ -27,16 +19,6 @@ use crate::{ ErrorKind, }; -use flate2::{Compression, GzBuilder}; -use nix::mount::{mount, umount, MsFlags}; - -pub const FLASHER_DEVICES: [&str; 5] = [ - DEV_TYPE_INTEL_NUC, - DEV_TYPE_GEN_X86_64, - DEV_TYPE_BBG, - DEV_TYPE_BBB, - DEV_TYPE_JETSON_XAVIER, -]; const SUPPORTED_DEVICES: [&str; 9] = [ DEV_TYPE_RPI3, DEV_TYPE_RPI2, @@ -49,12 +31,6 @@ const SUPPORTED_DEVICES: [&str; 9] = [ DEV_TYPE_JETSON_XAVIER, ]; -const IMG_NAME_GEN_X86_64: &str = "resin-image-genericx86-64-ext.resinos-img"; -const IMG_NAME_INTEL_NUC: &str = "resin-image-genericx86-64.resinos-img"; -const IMG_NAME_BBG: &str = "resin-image-beaglebone-green.resinos-img"; -const IMG_NAME_BBB: &str = "resin-image-beaglebone-black.resinos-img"; -const IMG_NAME_JETSON_XAVIER: &str = "balena-image-jetson-xavier.balenaos-img"; - fn parse_versions(versions: &Versions) -> Vec { let mut sem_vers: Vec = versions .iter() @@ -148,159 +124,6 @@ fn determine_version(ver_str: &str, versions: &Versions) -> Result { } } -pub(crate) fn extract_image, P2: AsRef>( - stream: Box, - image_file_name: P1, - device_type: &str, - work_dir: P2, -) -> Result<()> { - let work_dir = work_dir.as_ref(); - let progress = StreamProgress::new(stream, 10, Level::Info, None); - let mut disk = Disk::from_gzip_stream(progress)?; - let mut part_iterator = PartitionIterator::new(&mut disk)?; - if let Some(part_info) = part_iterator.nth(1) { - let mut reader = PartitionReader::from_part_iterator(&part_info, &mut part_iterator); - let extract_file_name = path_append(work_dir, "root_a.img"); - let mut tmp_file = File::create(&extract_file_name).upstream_with_context(&format!( - "Failed to create temporary file '{}'", - extract_file_name.display() - ))?; - - // TODO: show progress - copy(&mut reader, &mut tmp_file).upstream_with_context(&format!( - "Failed to extract root_a partition to temporary file '{}'", - extract_file_name.display() - ))?; - - info!("Finished root_a partition extraction, now mounting to extract balena OS image"); - - let mut loop_device = LoopDevice::for_file(&extract_file_name, None, None, None, true)?; - - debug!("loop device is '{}'", loop_device.get_path().display()); - - let mount_path = path_append(work_dir, "mnt_root_a"); - if !mount_path.exists() { - create_dir(&mount_path).upstream_with_context(&format!( - "Failed to create directory '{}'", - mount_path.display() - ))?; - } - - debug!("mount path is '{}'", mount_path.display()); - mount( - Some(loop_device.get_path()), - &mount_path, - Some(b"ext4".as_ref()), - MsFlags::empty(), - NIX_NONE, - ) - .upstream_with_context(&format!( - "Failed to mount '{}' on '{}", - loop_device.get_path().display(), - mount_path.display() - ))?; - - debug!("retrieving path for device type '{}'", device_type); - let img_path = match device_type { - DEV_TYPE_INTEL_NUC => path_append(path_append(&mount_path, "opt"), IMG_NAME_INTEL_NUC), - DEV_TYPE_GEN_X86_64 => { - path_append(path_append(&mount_path, "opt"), IMG_NAME_GEN_X86_64) - } - DEV_TYPE_BBB => path_append(path_append(&mount_path, "opt"), IMG_NAME_BBB), - DEV_TYPE_BBG => path_append(path_append(&mount_path, "opt"), IMG_NAME_BBG), - DEV_TYPE_JETSON_XAVIER => { - path_append(path_append(&mount_path, "opt"), IMG_NAME_JETSON_XAVIER) - } - _ => { - return Err(Error::with_context( - ErrorKind::InvParam, - &format!( - "Encountered undefined image name for device type {}", - device_type - ), - )); - } - }; - - debug!("image path is '{}'", img_path.display()); - let img_file_name = image_file_name.as_ref(); - - { - let mut gz_writer = GzBuilder::new().write( - File::create(img_file_name).upstream_with_context(&format!( - "Failed to open image file for writing: '{}'", - img_file_name.display() - ))?, - Compression::best(), - ); - - let img_reader = OpenOptions::new() - .read(true) - .open(&img_path) - .upstream_with_context(&format!( - "Failed to open image file for reading: '{}'", - img_path.display() - ))?; - - info!("Recompressing OS image to {}", img_file_name.display()); - - let size = if let Ok(metadata) = img_reader.metadata() { - Some(metadata.len()) - } else { - None - }; - - let mut stream_progress = StreamProgress::new(img_reader, 10, Level::Info, size); - - copy(&mut stream_progress, &mut gz_writer).upstream_with_context(&format!( - "Failed to compress image '{}' to '{}'", - img_path.display(), - img_file_name.display() - ))?; - } - - info!( - "The balena OS image was successfully written to '{}', cleaning up", - img_file_name.display() - ); - - match umount(&mount_path) { - Ok(_) => { - if let Err(why) = remove_dir(&mount_path) { - warn!( - "Failed to remove mount temporary directory '{}', error: {:?}", - mount_path.display(), - why - ); - } - } - Err(why) => { - warn!( - "Failed to unmount temporary mount from '{}', error: {:?}", - mount_path.display(), - why - ); - } - } - - loop_device.unset()?; - - if let Err(why) = fs::remove_file(&extract_file_name) { - warn!( - "Failed to remove extracted partition '{}', error: {:?}", - extract_file_name.display(), - why - ); - } - Ok(()) - } else { - Err(Error::with_context( - ErrorKind::InvState, - "Failed to find root_a partition in downloaded image", - )) - } -} - pub(crate) fn download_image( opts: &Options, balena_cfg: &BalenaCfgJson, @@ -351,30 +174,22 @@ pub(crate) fn download_image( format!("balena-cloud-{}-{}.img.gz", device_type, version), ); - if FLASHER_DEVICES.contains(&device_type) { - if !is_admin()? { - error!("please run this program as root"); - return Err(Error::displayed()); - } - extract_image(stream, &img_file_name, device_type, work_dir)?; - } else { - debug!("Downloading file '{}'", img_file_name.display()); - let mut file = File::create(&img_file_name).upstream_with_context(&format!( - "Failed to create file: '{}'", - img_file_name.display() - ))?; - - // TODO: show progress - let mut progress = StreamProgress::new(stream, 10, Level::Info, None); - copy(&mut progress, &mut file).upstream_with_context(&format!( - "Failed to write downloaded data to '{}'", - img_file_name.display() - ))?; - info!( - "The balena OS image was successfully written to '{}'", - img_file_name.display() - ); - } + debug!("Downloading file '{}'", img_file_name.display()); + let mut file = File::create(&img_file_name).upstream_with_context(&format!( + "Failed to create file: '{}'", + img_file_name.display() + ))?; + + // TODO: show progress + let mut progress = StreamProgress::new(stream, 10, Level::Info, None); + copy(&mut progress, &mut file).upstream_with_context(&format!( + "Failed to write downloaded data to '{}'", + img_file_name.display() + ))?; + info!( + "The balena OS image was successfully written to '{}'", + img_file_name.display() + ); Ok(img_file_name) }