iot_device_bridge 1.1.1

Bridge between messaging of the device and the cloud IoT (e.g., AWS).
Documentation
//! Config utilities for loading and storing settings from/to YAML file.
//!
//! The config.yaml has sections specific for device connection and processing
//! and for IoT connection and processing.
//! These sections correspond to the `DeviceConfig`and `IoTConfig`.

use crate::error::IoTError;
use find_folder::Search;
use log::debug;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs::File;

pub const SHADOWS_NUM: usize = 2; // number of shadows (for array capacity)
pub const CONFIG_DIRNAME: &str = "device-iot.config";
pub const YAML_FILENAME: &str = "config.yaml";
pub const CERTS_SUBDIRNAME: &str = "certs";
pub const IOTREGISTRATIONSTATUS_INITIAL: &str = "INITIAL";
pub const IOTREGISTRATIONSTATUS_REGISTERED: &str = "REGISTERED";
pub const IOTREGISTRATIONSTATUS_REGISTERED_SEPARATOR: &str = "REGISTERED:";
pub const IOTREGISTRATIONSTATUS_CERTIFICATE_ROTATION_REQUESTED: &str =
    "CERTIFICATE_ROTATION_REQUESTED";
pub const IOTREGISTRATIONSTATUS_CERTIFICATE_RECEIVED: &str = "CERTIFICATE_RECEIVED";
pub const IOTREGISTRATIONSTATUS_CSR_ERROR: &str = "CSR_ERROR";
pub const IOTREGISTRATIONSTATUS_REGISTRATION_ERROR: &str = "REGISTRATION_ERROR";
pub const IOTREGISTRATIONSTATUS_UNKNOWN_IOTREGISTRATIONSTATUS: &str =
    "UNKNOWN_IotRegistrationStatus";

/// IoT registration status
///   - `Initial` at start of the Fleet Provisioning for specific device
///   - `Registered(String)` at completion of the Fleet Provisioning with ThingName as parameter
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum IotRegistrationStatus {
    Initial,
    CertReceived,
    Registered(String),
    CsrError,
    RegistrationError,
    CertificateRotationRequested,
    Unknown,
}

impl std::fmt::Display for IotRegistrationStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            IotRegistrationStatus::Initial => write!(f, "{}", IOTREGISTRATIONSTATUS_INITIAL),
            IotRegistrationStatus::CertReceived => write!(f, "CERTIFICATE_RECEIVED"),
            IotRegistrationStatus::Registered(t) => {
                write!(f, "{}:{}", IOTREGISTRATIONSTATUS_REGISTERED, t)
            }
            IotRegistrationStatus::CsrError => write!(f, "CSR_ERROR"),
            IotRegistrationStatus::RegistrationError => write!(f, "REGISTRATION_ERROR"),
            IotRegistrationStatus::CertificateRotationRequested => write!(
                f,
                "{}",
                IOTREGISTRATIONSTATUS_CERTIFICATE_ROTATION_REQUESTED
            ),
            IotRegistrationStatus::Unknown => write!(f, "UNKNOWN_IotRegistrationStatus"),
        }
    }
}

impl std::str::FromStr for IotRegistrationStatus {
    type Err = IoTError;

    fn from_str(s: &str) -> ::core::result::Result<Self, Self::Err> {
        let ss = s.to_string();
        if ss.starts_with(IOTREGISTRATIONSTATUS_REGISTERED) {
            match ss.strip_prefix(IOTREGISTRATIONSTATUS_REGISTERED_SEPARATOR) {
                Some(thing_name) => ::core::result::Result::Ok(IotRegistrationStatus::Registered(
                    thing_name.to_string(),
                )),
                None => ::core::result::Result::Err(IoTError::IotStatusParsingError),
            }
        } else {
            match s {
                IOTREGISTRATIONSTATUS_INITIAL => {
                    ::core::result::Result::Ok(IotRegistrationStatus::Initial)
                }
                IOTREGISTRATIONSTATUS_CERTIFICATE_ROTATION_REQUESTED => {
                    ::core::result::Result::Ok(IotRegistrationStatus::CertificateRotationRequested)
                }
                IOTREGISTRATIONSTATUS_CERTIFICATE_RECEIVED => {
                    ::core::result::Result::Ok(IotRegistrationStatus::CertReceived)
                }
                IOTREGISTRATIONSTATUS_CSR_ERROR => {
                    ::core::result::Result::Ok(IotRegistrationStatus::CsrError)
                }
                IOTREGISTRATIONSTATUS_REGISTRATION_ERROR => {
                    ::core::result::Result::Ok(IotRegistrationStatus::RegistrationError)
                }
                IOTREGISTRATIONSTATUS_UNKNOWN_IOTREGISTRATIONSTATUS => {
                    ::core::result::Result::Ok(IotRegistrationStatus::Unknown)
                }
                _ => ::core::result::Result::Err(IoTError::IotStatusParsingError),
            }
        }
    }
}

/// `DeviceConfig` defines structure for device specific parameters
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct DeviceConfig {
    pub device_topic_prefix: String,
    pub shadow_name: String,
    pub client_id: String,
    pub endpoint: String,
    pub port: u16,
    pub username: String,
    pub password: String,
    pub spec_version: String,
    pub privacy: bool,
    pub rudi_gtin: String,
    pub rudi_ref: String,
    pub instrument_type: String,
    pub instrument_name: String,
    pub instrument_serial_number: String,
    pub source_id_type: String,
}

/// `IoTConfig` defines structure for IoT specific parameters and cloud connectivity
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct IoTConfig {
    pub iot_topic_prefix: String,
    pub shadow_name: String,
    pub client_registration_status: String,
    pub client_id: String,
    pub endpoint: String,
    pub port: u16,
    pub ca_path: String,
    pub client_cert_path: String,
    pub client_priv_key_path: String,
    pub client_pub_key_path: String,
    pub claim_cert_path: String,
    pub claim_priv_key_path: String,
    pub claim_pub_key_path: String,
    pub provisioning_template_name: String,
    // pub shadow_names: [String; SHADOWS_NUM]
}

/// `Config` defines structure and function for storing and accessing configuration in YAML file
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct Config {
    pub device: DeviceConfig,
    pub iot: IoTConfig,
}

fn set_abs_paths(config: &mut Config, conf_path_str: String) {
    config.iot.ca_path = format!(
        "{}/{}/{}",
        conf_path_str, CERTS_SUBDIRNAME, config.iot.ca_path
    );
    config.iot.client_cert_path = format!(
        "{}/{}/{}",
        conf_path_str, CERTS_SUBDIRNAME, config.iot.client_cert_path
    );
    config.iot.client_priv_key_path = format!(
        "{}/{}/{}",
        conf_path_str, CERTS_SUBDIRNAME, config.iot.client_priv_key_path
    );
    config.iot.client_pub_key_path = format!(
        "{}/{}/{}",
        conf_path_str, CERTS_SUBDIRNAME, config.iot.client_pub_key_path
    );
    config.iot.claim_cert_path = format!(
        "{}/{}/{}",
        conf_path_str, CERTS_SUBDIRNAME, config.iot.claim_cert_path
    );
    config.iot.claim_priv_key_path = format!(
        "{}/{}/{}",
        conf_path_str, CERTS_SUBDIRNAME, config.iot.claim_priv_key_path
    );
    config.iot.claim_pub_key_path = format!(
        "{}/{}/{}",
        conf_path_str, CERTS_SUBDIRNAME, config.iot.claim_pub_key_path
    );
}

impl Config {
    pub fn set_abs_path_client_cert(&mut self) {
        self.iot.client_cert_path = format!(
            "{}/{}/{}",
            CONFIG_LOCATION.as_path().display().to_string(),
            CERTS_SUBDIRNAME,
            self.iot.client_cert_path
        );
    }

    pub fn get_config_from_yaml() -> Result<Config, IoTError> {
        let conf_path_str = CONFIG_LOCATION.as_path().display().to_string();
        let config_yaml_path = format!("{}/{}", conf_path_str, YAML_FILENAME);
        debug!("CONFIG_PATH_YAML_FILENAME -- GET: {}", config_yaml_path);

        let mut config: Config = serde_yaml::from_reader(&File::open(
            std::path::Path::new(&config_yaml_path).to_path_buf(),
        )?)?;

        set_abs_paths(&mut config, conf_path_str);

        Ok(config)
    }

    pub fn store_config_to_yaml(&mut self) -> Result<(), IoTError> {
        if let Some(p) = self.iot.ca_path.rsplit_once('/') {
            self.iot.ca_path = String::from(p.1)
        };
        if let Some(p) = self.iot.client_cert_path.rsplit_once('/') {
            self.iot.client_cert_path = String::from(p.1)
        };
        if let Some(p) = self.iot.client_priv_key_path.rsplit_once('/') {
            self.iot.client_priv_key_path = String::from(p.1)
        };
        if let Some(p) = self.iot.client_pub_key_path.rsplit_once('/') {
            self.iot.client_pub_key_path = String::from(p.1)
        };
        if let Some(p) = self.iot.claim_cert_path.rsplit_once('/') {
            self.iot.claim_cert_path = String::from(p.1)
        };
        if let Some(p) = self.iot.claim_priv_key_path.rsplit_once('/') {
            self.iot.claim_priv_key_path = String::from(p.1)
        };
        if let Some(p) = self.iot.claim_pub_key_path.rsplit_once('/') {
            self.iot.claim_pub_key_path = String::from(p.1)
        };

        let config_yaml_path = format!(
            "{}/{}",
            CONFIG_LOCATION.as_path().display().to_string(),
            YAML_FILENAME
        );
        debug!(
            "\nCONFIG_PATH_YAML_FILENAME -- STORE: {}\n",
            config_yaml_path
        );

        serde_yaml::to_writer(
            &File::create(std::path::Path::new(&config_yaml_path).to_path_buf())?,
            &self,
        )?;

        Ok(())
    }
}

/// CONFIG_LOCATION is initialized path to the config directory
pub static CONFIG_LOCATION: Lazy<std::path::PathBuf> = Lazy::new(|| {
    let mut exe_folder = env::current_exe().unwrap();
    exe_folder.pop(); // Remove the executable's name, leaving the path to the containing folder
    Search::ParentsThenKids(5, 5)
        .of(exe_folder)
        .for_folder(CONFIG_DIRNAME)
        .expect("Config directory not found")
});

pub fn build_thing_name(config: &Config) -> String {
    format!(
        "{}_{}",
        config.device.instrument_type, 
        config.device.instrument_serial_number
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn store_and_get_config_yaml_test() {
        let dev_config = DeviceConfig {
            device_topic_prefix: String::from("SPDIF/X320/Poke/"),
            shadow_name: String::from("c16a8_shadow"),
            client_id: String::from("adapterClient"),
            endpoint: String::from("127.0.0.1"),
            port: 1883,
            username: String::from("username"),
            password: String::from("password"),
            spec_version: String::from("1.0"),
            privacy: false,
            rudi_gtin: String::from("8724447281187"),
            rudi_ref: String::from("9818575112"),
            instrument_type: String::from("16A8"),
            instrument_name: String::from("Friendly_Name"),
            instrument_serial_number: String::from("99999"),
            source_id_type: String::from("CONFIG"),
        };

        let iot_config = IoTConfig {
            iot_topic_prefix: String::from("SPDIF/X320/16A8/"),
            shadow_name: String::from("iot_shadow"),
            client_registration_status: String::from("INITIAL"),
            client_id: String::from("16A8_99998"),
            endpoint: String::from("ENDPOINTID-ats.iot.eu-central-1.amazonaws.com"),
            port: 8883,
            ca_path: String::from("AmazonRootCA1.pem"),
            client_cert_path: String::from("IotCertificate.pem"),
            client_priv_key_path: String::from("IotPrivateKey.pem"),
            client_pub_key_path: String::from("IotPubKey.pem"),
            claim_cert_path: String::from("ClaimCertificate.pem"),
            claim_priv_key_path: String::from("ClaimPrivateKey.pem"),
            claim_pub_key_path: String::from("ClaimPubKey.pem"),
            provisioning_template_name: String::from("iot-16A8-prov-templ"),
        };

        let mut config = Config {
            device: dev_config,
            iot: iot_config,
        };

        config
            .store_config_to_yaml()
            .expect("problem storing \"config.yaml\"");

        let config_read = Config::get_config_from_yaml().expect("Problem reading \"config.yaml\"");

        assert_eq!(config.device, config_read.device);
        assert_eq!(config.iot.endpoint, config_read.iot.endpoint);
        let config_certs_capath = format!(
            "{}/certs/{}",
            CONFIG_LOCATION.as_path().display().to_string(),
            config.iot.ca_path
        );
        assert_eq!(config_certs_capath, config_read.iot.ca_path);
    }
}