factory_fai: implement collect data function

Implement the function to collect simple FAI data, and leave TODO items
for some complicated cases.

BUG=b:230061675
TEST=cargo test
TEST=boot factory shim on DUT and perform FAI.

Cq-Depend: chromium:3626980, chromium:3653785
Change-Id: I266243c32152b4b2d05065f42a4517ffcc9e9bac
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/factory_installer/+/3627383
Reviewed-by: Meng-Huan Yu <menghuan@chromium.org>
Tested-by: Yu-An Wang <wyuang@google.com>
Reviewed-by: Stimim Chen <stimim@chromium.org>
Commit-Queue: Yu-An Wang <wyuang@google.com>
diff --git a/factory_install.sh b/factory_install.sh
index a0499d5..36b1c78 100644
--- a/factory_install.sh
+++ b/factory_install.sh
@@ -70,7 +70,7 @@
 # Each action x is implemented in an action_$x handler (e.g.,
 # action_i); see the handlers for more information about what
 # each option is.
-SUPPORTED_ACTIONS=bcdeimrstuvyz
+SUPPORTED_ACTIONS=bcdefimrstuvyz
 SUPPORTED_ACTIONS_BOARD=
 
 # Supported actions when RSU is required.
@@ -1152,6 +1152,9 @@
   menu_line U "Update TPM firmware" "Update TPM firmware"
   menu_line E "Perform RSU" "Perform RSU (RMA Server Unlock)"
   menu_line M "Enable factory mode" "Enable TPM factory mode"
+  # TODO(wyuang): show up in menu after all FAI functions ready.
+  # menu_line F "Perform factory FAI" \
+  #             "Perform Factory FAI(First Article Inspection)"
 
   menu_board
 
@@ -1498,6 +1501,11 @@
   tpm_enable_factory_mode
 }
 
+# F = Perform Factory FAI process
+action_f() {
+  /usr/sbin/factory_fai
+}
+
 try_set_default_action_rsu() {
   if is_rsu_required; then
     log "RSU is required. " \
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 21cfd36..1ee0b0c 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -4,12 +4,15 @@
 edition = "2021"
 
 [dependencies]
-anyhow = { version = "1.0.0", optional = true }
-serde = { version = "1.0.0", features = ["derive"], optional = true }
-glob = { version = "0.3.0", optional = true }
-tempfile = { version = "3.2.0", optional = true }
-bincode = { version = "1.0.1", optional = true }
+anyhow = "1.0.0"
+bincode = "1.0.1"
+clap = { version = "3.1.0", features = ["derive"] }
+glob = "0.3.0"
+serde = { version = "1.0.0", features = ["derive"] }
+serde_json = "1.0.0"
+tempfile = "3.2.0"
 
 [features]
+default = ["factory-fai", "factory-ufs"]
 factory-fai = []
-factory-ufs = ["anyhow", "serde", "glob", "tempfile", "bincode"]
+factory-ufs = []
diff --git a/rust/src/bin/factory_fai.rs b/rust/src/bin/factory_fai.rs
index f3c87b4..4044515 100644
--- a/rust/src/bin/factory_fai.rs
+++ b/rust/src/bin/factory_fai.rs
@@ -2,7 +2,30 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// TODO(wyuang): implement factory FAI.
-fn main() {
-    println!("Factory FAI is not available yet.");
+use std::fs;
+
+use factory_installer::factory_fai;
+use factory_installer::factory_fai::args::{Args, Parser};
+use factory_installer::factory_fai::config;
+
+use anyhow::{Context, Result};
+
+fn main() -> Result<()> {
+    let args = Args::parse();
+
+    if args.dump_config {
+        println!("{}", config::DEFAULT_CONFIG);
+        return Ok(());
+    }
+
+    let fai_config = config::load_config(args.config_path).context("Failed to load config.")?;
+
+    let data = factory_fai::collect_fai_data(fai_config)?;
+
+    if let Some(path) = args.output_path {
+        println!("Writing result to {}.", path);
+        fs::write(path, &data).context("Failed to write result to output file.")?;
+    }
+    println!("{}", &data);
+    Ok(())
 }
diff --git a/rust/src/factory_fai/args.rs b/rust/src/factory_fai/args.rs
new file mode 100644
index 0000000..d3520a2
--- /dev/null
+++ b/rust/src/factory_fai/args.rs
@@ -0,0 +1,22 @@
+// Copyright 2022 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+pub use clap::Parser;
+
+/// ChromeOS Factory First Article Inspection Process.
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+pub struct Args {
+    /// Output the collected data to the file path
+    #[clap(short, long)]
+    pub output_path: Option<String>,
+
+    /// Path of config file.
+    #[clap(short, long)]
+    pub config_path: Option<String>,
+
+    /// Dump the default configuration.
+    #[clap(long)]
+    pub dump_config: bool,
+}
diff --git a/rust/src/factory_fai/config.rs b/rust/src/factory_fai/config.rs
new file mode 100644
index 0000000..5fd8014
--- /dev/null
+++ b/rust/src/factory_fai/config.rs
@@ -0,0 +1,113 @@
+// Copyright 2022 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use std::collections::HashMap;
+use std::fs;
+use std::path::Path;
+
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+use serde_json;
+
+// TODO(wyuang): collect more default data:
+// 1. Partition Table ("cgpt show ...")
+// 2. Release image info ("mount $DEV; cat /$DEV/etc/lsb-release")
+// 3. Signing key
+// 4. CBI data ("ectool cbi get <tag> ...")
+// TODO(wyuang): VPD may contains "\n", we would probably want to read values from
+// `/sys/firmware/vpd` instead of `vpd` command.
+pub const DEFAULT_CONFIG: &str = r##"{
+  "ro_vpd": {
+    "DataCommand": {
+      "cmd": "vpd",
+      "args": ["-i", "RO_VPD", "-l"]
+    }
+  },
+  "rw_vpd": {
+    "DataCommand": {
+      "cmd": "vpd",
+      "args": ["-i", "RW_VPD", "-l"]
+    }
+  },
+  "stateful_partition": {
+    "DataCommand": {
+      "cmd": "find",
+      "args": ["/mnt/stateful_partition", "-not", "-type", "d"]
+    }
+  },
+  "tpm_version": {
+    "DataCommand": {
+      "cmd": "tpm_version"
+    }
+  },
+  "gsc_capabilities": {
+    "DataCommand": {
+      "cmd": "gsctool",
+      "args": ["-a", "-I", "-M"]
+    }
+  },
+  "gsc_board_id": {
+    "DataCommand": {
+      "cmd": "gsctool",
+      "args": ["-a", "-i", "-M"]
+    }
+  },
+  "gsc_firmware_version": {
+    "DataCommand": {
+      "cmd": "gsctool",
+      "args": ["-a", "-f", "-M"]
+    }
+  },
+  "crossystem": {
+    "DataCommand": {
+      "cmd": "crossystem"
+    }
+  },
+  "ap_write_protect": {
+    "DataCommand": {
+      "cmd": "flashrom",
+      "args": ["-p", "host", "--wp-status"]
+    }
+  },
+  "ec_write_protect": {
+    "DataCommand": {
+      "cmd": "flashrom",
+      "args": ["-p", "ec", "--wp-status"]
+    }
+  },
+  "factory_instal_shim_version": {
+    "DataCommand": {
+      "cmd": "cat",
+      "args": ["/etc/lsb-release"]
+    }
+  }
+}"##;
+
+#[derive(Serialize, Deserialize)]
+pub struct DataCommand {
+    pub cmd: String,
+
+    #[serde(default)]
+    pub args: Vec<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub enum DataCollector {
+    // TODO(wyuang): support DataFunction and DataFiles for calling function and read files.
+    DataCommand(DataCommand),
+}
+
+pub type FAIConfig = HashMap<String, DataCollector>;
+
+fn load_config_file<P: AsRef<Path>>(path: P) -> Result<FAIConfig> {
+    let config_content = fs::read_to_string(path)?;
+    Ok(serde_json::from_str(&config_content)?)
+}
+
+pub fn load_config<P: AsRef<Path>>(config_path: Option<P>) -> Result<FAIConfig> {
+    match config_path {
+        Some(file_path) => load_config_file(&file_path),
+        None => Ok(serde_json::from_str(DEFAULT_CONFIG)?),
+    }
+}
diff --git a/rust/src/factory_fai/mod.rs b/rust/src/factory_fai/mod.rs
new file mode 100644
index 0000000..b105299
--- /dev/null
+++ b/rust/src/factory_fai/mod.rs
@@ -0,0 +1,37 @@
+// Copyright 2022 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use crate::factory_fai::config::{DataCollector, FAIConfig};
+use crate::utils::process_utils::{Command, StringOutput};
+
+use anyhow::{Context, Result};
+use serde_json;
+use serde_json::{Map, Value};
+
+pub mod args;
+pub mod config;
+
+fn default_parse_function(output: String) -> Value {
+    // TODO(wyuang): implement customized parse functions for different
+    // collected data.
+    Value::String(output.trim().to_string())
+}
+
+pub fn collect_fai_data(fai_config: FAIConfig) -> Result<String> {
+    let mut fai_data = Map::new();
+    for (name, config) in fai_config.iter() {
+        let data = match config {
+            DataCollector::DataCommand(data_cmd) => {
+                let output = Command::new(&data_cmd.cmd)
+                    .args(&data_cmd.args)
+                    .output()
+                    .with_context(|| format!("Failed to collect \"{}\".", name))?;
+                default_parse_function(output.stdout())
+            }
+        };
+
+        fai_data.insert(name.to_string(), data);
+    }
+    Ok(serde_json::to_string_pretty(&fai_data)?)
+}
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index f4aadd5..7fadc71 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -4,4 +4,8 @@
 
 #[cfg(feature = "factory-ufs")]
 pub mod factory_ufs;
+
+#[cfg(feature = "factory-fai")]
+pub mod factory_fai;
+
 pub mod utils;
diff --git a/rust/tests/factory_fai/test_config.json b/rust/tests/factory_fai/test_config.json
new file mode 100644
index 0000000..a4e27c9
--- /dev/null
+++ b/rust/tests/factory_fai/test_config.json
@@ -0,0 +1,13 @@
+{
+  "data1": {
+    "DataCommand": {
+      "cmd": "echo",
+       "args": ["some", "data"]
+    }
+  },
+  "data2": {
+    "DataCommand": {
+        "cmd": "echo"
+    }
+  }
+}
diff --git a/rust/tests/factory_fai/test_expected_data.json b/rust/tests/factory_fai/test_expected_data.json
new file mode 100644
index 0000000..52ffc11
--- /dev/null
+++ b/rust/tests/factory_fai/test_expected_data.json
@@ -0,0 +1,4 @@
+{
+  "data1": "some data",
+  "data2": ""
+}
diff --git a/rust/tests/factory_fai/test_factory_fai.rs b/rust/tests/factory_fai/test_factory_fai.rs
new file mode 100644
index 0000000..0bd5bdc
--- /dev/null
+++ b/rust/tests/factory_fai/test_factory_fai.rs
@@ -0,0 +1,21 @@
+// Copyright 2022 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use std::fs;
+
+use factory_installer::factory_fai;
+use factory_installer::factory_fai::config;
+
+const TEST_CONFIG_PATH: &str = "tests/factory_fai/test_config.json";
+const TEST_EXPECTED_DATA_PATH: &str = "tests/factory_fai/test_expected_data.json";
+
+#[test]
+fn test_collect_fai_data_success() {
+    let output =
+        factory_fai::collect_fai_data(config::load_config(Some(TEST_CONFIG_PATH)).unwrap())
+            .unwrap();
+    let expected = fs::read_to_string(TEST_EXPECTED_DATA_PATH).unwrap();
+
+    assert_eq!(expected.trim(), output.trim());
+}
diff --git a/rust/tests/tests.rs b/rust/tests/tests.rs
index 76b5cdd..df80dbe 100644
--- a/rust/tests/tests.rs
+++ b/rust/tests/tests.rs
@@ -6,3 +6,8 @@
 mod utils {
     mod test_process_utils;
 }
+
+#[cfg(test)]
+mod factory_fai {
+    mod test_factory_fai;
+}