Skip to content
This repository has been archived by the owner on Jun 23, 2022. It is now read-only.

*: Data collection from FCOS machines #31

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ liboverdrop = "^0.0.2"
log = "^0.4.6"
serde = { version = "^1.0.91", features = ["derive"] }
toml = "^0.5.1"
maplit = "^1.0"
serde_json = "1.0.40"

[package.metadata.release]
sign-commit = true
Expand Down
148 changes: 148 additions & 0 deletions src/identity/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
mod platform;
mod os_release;

use crate::config::inputs;
use failure::{Fallible, ResultExt};
use serde::Serialize;
use std::collections::HashMap;
use maplit;

/// Kernel arguments location
static KERNEL_ARGS_FILE: &str = "/proc/cmdline";
/// OS release file location
static OS_RELEASE_FILE: &str = "/etc/os-release";
/// OS alpha version file
static OS_ALPHA_VERSION_FILE: &str = "/.coreos-aleph-version.json";
zonggen marked this conversation as resolved.
Show resolved Hide resolved

/// Agent identity.
#[derive(Debug, Serialize)]
pub(crate) struct Identity {
/// Collecting level
pub(crate) level: String,
/// OS platform
pub(crate) platform: String,
/// Original OS version
pub(crate) original_os_version: String,
/// Current OS version
pub(crate) current_os_version: String,
}

impl Identity {
/// Create from configuration.
pub(crate) fn new(cfg: &inputs::CollectingInput) -> Fallible<Self> {
let collecting_level = &cfg.level;
let id = match collecting_level.as_str() {
level @ "minimal" | level @ "full" => Self::try_default(level).context(format!("failed to build '{}' identity", level))?,
&_ => Self::try_default("minimal").context("failed to build 'minimal' identity")?,
};

Ok(id)
}

/// Try to fetch default data
pub fn try_default(level: &str) -> Fallible<Self> {
let platform = platform::read_id(KERNEL_ARGS_FILE)?;
let original_os_version = os_release::read_original_os_version(OS_ALPHA_VERSION_FILE)?;
let current_os_version = os_release::read_current_os_version(OS_RELEASE_FILE)?;

let id = match level {
zonggen marked this conversation as resolved.
Show resolved Hide resolved
"minimal" | "full" => Self {
level: level.to_string(),
platform,
original_os_version,
current_os_version,
},
&_ => Self {
level: "minimal".to_string(),
platform,
original_os_version,
current_os_version,
},
};

Ok(id)
}

/// Getter for collected data, returned as a HashMap
pub fn get_data(&self) -> HashMap<String, String> {
let vars = maplit::hashmap!{
"level".to_string() => self.level.clone(),
"platform".to_string() => self.platform.clone(),
"original_os_version".to_string() => self.original_os_version.clone(),
"current_os_version".to_string() => self.current_os_version.clone(),
};

// TODO: Insert data specific to different levels
match self.level.as_str() {
"minimal" | "full" => (),
&_ => (),
};

vars
}

#[cfg(test)]
pub(crate) fn mock_default(level: &str) -> Self {
match level {
"minimal" => return Self {
level: String::from("minimal"),
platform: "mock-qemu".to_string(),
original_os_version: "30.20190923.dev.2-2".to_string(),
current_os_version: "mock-os-version".to_string(),
},
"full" => return Self {
level: String::from("full"),
platform: "mock-gcp".to_string(),
original_os_version: "30.20190923.dev.2-2".to_string(),
current_os_version: "mock-os-version".to_string(),
},
&_ => return Self {
level: String::from("minimal"),
platform: "mock-qemu".to_string(),
original_os_version: "30.20190923.dev.2-2".to_string(),
current_os_version: "mock-os-version".to_string(),
},
}
}
}

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

#[test]
fn test_minimal() {
let id = Identity::mock_default("minimal");
let vars = id.get_data();

// check if the keys exist
assert!(vars.contains_key("level"));
assert!(vars.contains_key("platform"));
assert!(vars.contains_key("original_os_version"));
assert!(vars.contains_key("current_os_version"));

// check if the values match
assert_eq!(vars.get("level"), Some(&"minimal".to_string()));
assert_eq!(vars.get("platform"), Some(&"mock-qemu".to_string()));
assert_eq!(vars.get("original_os_version"), Some(&"30.20190923.dev.2-2".to_string()));
assert_eq!(vars.get("current_os_version"), Some(&"mock-os-version".to_string()));
}

#[test]
fn test_full() {
let id = Identity::mock_default("full");
let vars = id.get_data();

// check if the keys exist
assert!(vars.contains_key("level"));
assert!(vars.contains_key("platform"));
assert!(vars.contains_key("original_os_version"));
assert!(vars.contains_key("current_os_version"));

// check if the values match
assert_eq!(vars.get("level"), Some(&"full".to_string()));
assert_eq!(vars.get("platform"), Some(&"mock-gcp".to_string()));
assert_eq!(vars.get("original_os_version"), Some(&"30.20190923.dev.2-2".to_string()));
assert_eq!(vars.get("current_os_version"), Some(&"mock-os-version".to_string()));
}
}
114 changes: 114 additions & 0 deletions src/identity/os_release.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! OS version parsing - utility functions

use failure::{bail, format_err, Fallible, ResultExt};
use std::io::Read;
use std::{fs, io};
use serde_json;

/// OS version flag.
static OS_VERSION_FLAG: &str = "VERSION";

/// Read original os version info from os alpha version json file.
pub(crate) fn read_original_os_version<T>(file_path: T) -> Fallible<String>
where
T: AsRef<str>,
{
// open the os release file
let fpath = file_path.as_ref();
let file = fs::File::open(fpath)
.with_context(|e| format_err!("failed to open alpha version file {}: {}", fpath, e))?;

// parse the content
let json: serde_json::Value = serde_json::from_reader(file)
.expect("failed to parse alpha version file as JSON");
let build: String = json.get("build")
.expect("alpha version file does not contain 'build' key")
.to_string();

Ok(build)

}

/// Read current os version info from os release file.
zonggen marked this conversation as resolved.
Show resolved Hide resolved
pub(crate) fn read_current_os_version<T>(file_path: T) -> Fallible<String>
where
T: AsRef<str>,
{
// open the os release file
let fpath = file_path.as_ref();
let file = fs::File::open(fpath)
.with_context(|e| format_err!("failed to open os-release file {}: {}", fpath, e))?;

// read content
let mut bufrd = io::BufReader::new(file);
let mut contents = String::new();
bufrd
.read_to_string(&mut contents)
.with_context(|e| format_err!("failed to read os-release file {}: {}", fpath, e))?;

// lookup flag by key name
match find_flag_value(OS_VERSION_FLAG, &contents) {
Some(version) => {
log::trace!("found os version: {}", version);
Ok(version)
}
None => bail!(
"could not find flag '{}' in {}",
OS_VERSION_FLAG,
fpath
),
}
}

/// Find VERSION flag in os-release contents.
fn find_flag_value(flagname: &str, contents: &str) -> Option<String> {
// split contents into elements and keep key-value tuples only.
let params: Vec<(&str, &str)> = contents
.split('\n')
.filter_map(|s| {
let v: Vec<&str> = s.splitn(2, '=').collect();
match v.len() {
2 => Some((v[0], v[1])),
_ => None,
}
})
.collect();

// find the OS release flag
for (key, val) in params {
if key != flagname {
continue;
}
let bare_val = val.trim();
if !bare_val.is_empty() {
return Some(bare_val.to_string());
}
}
None
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_flag_os_version() {
let flagname = "VERSION";
let tests = vec![
("", None),
("foo=bar", None),
("VERSION", None),
("VERSION=", None),
("VERSION=\t", None),
("VERSION=\"30.20190905.dev.2 (CoreOS preview)\"", Some("\"30.20190905.dev.2 (CoreOS preview)\"".to_string())),
("VERSION=\t\"30.20190905.dev.2 (CoreOS preview)\"", Some("\"30.20190905.dev.2 (CoreOS preview)\"".to_string())),
("VERSION=\"30.20190905.dev.2 (CoreOS preview)\"\n", Some("\"30.20190905.dev.2 (CoreOS preview)\"".to_string())),
("foo=bar\nVERSION=\"30.20190905.dev.2 (CoreOS preview)\"", Some("\"30.20190905.dev.2 (CoreOS preview)\"".to_string())),
("VERSION=\"30.20190905.dev.2 (CoreOS preview)\"\nfoo=bar", Some("\"30.20190905.dev.2 (CoreOS preview)\"".to_string())),
("foo=bar\nVERSION=\"30.20190905.dev.2 (CoreOS preview)\"\nfoo=bar", Some("\"30.20190905.dev.2 (CoreOS preview)\"".to_string())),
];
for (tcase, tres) in tests {
let res = find_flag_value(flagname, tcase);
assert_eq!(res, tres, "failed testcase: '{}'", tcase);
}
}
}
97 changes: 97 additions & 0 deletions src/identity/platform.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//! Kernel cmdline parsing - utility functions
//!
//! NOTE(lucab): this is not a complete/correct cmdline parser, as it implements
zonggen marked this conversation as resolved.
Show resolved Hide resolved
//! just enough logic to extract the platform ID value. In particular, it does not
//! handle separator quoting/escaping, list of values, and merging of repeated
//! flags. Logic is taken from Afterburn, please backport any bugfix there too:
//! https://github.com/coreos/afterburn/blob/v4.1.0/src/util/cmdline.rs

use failure::{bail, format_err, Fallible, ResultExt};
use std::io::Read;
use std::{fs, io};

/// Platform key.
static CMDLINE_PLATFORM_FLAG: &str = "ignition.platform.id";

/// Read platform value from cmdline file.
pub(crate) fn read_id<T>(cmdline_path: T) -> Fallible<String>
where
T: AsRef<str>,
{
// open the cmdline file
let fpath = cmdline_path.as_ref();
let file = fs::File::open(fpath)
.with_context(|e| format_err!("failed to open cmdline file {}: {}", fpath, e))?;

// read content
let mut bufrd = io::BufReader::new(file);
let mut contents = String::new();
bufrd
.read_to_string(&mut contents)
.with_context(|e| format_err!("failed to read cmdline file {}: {}", fpath, e))?;

// lookup flag by key name
match find_flag_value(CMDLINE_PLATFORM_FLAG, &contents) {
Some(platform) => {
log::trace!("found platform id: {}", platform);
Ok(platform)
}
None => bail!(
"could not find flag '{}' in {}",
CMDLINE_PLATFORM_FLAG,
fpath
),
}
}

/// Find OEM ID flag value in cmdline string.
fn find_flag_value(flagname: &str, cmdline: &str) -> Option<String> {
// split content into elements and keep key-value tuples only.
let params: Vec<(&str, &str)> = cmdline
.split(' ')
.filter_map(|s| {
let kv: Vec<&str> = s.splitn(2, '=').collect();
match kv.len() {
2 => Some((kv[0], kv[1])),
_ => None,
}
})
.collect();

// find the oem flag
for (key, val) in params {
if key != flagname {
continue;
}
let bare_val = val.trim();
if !bare_val.is_empty() {
return Some(bare_val.to_string());
}
}
None
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_flag_platform() {
let flagname = "ignition.platform.id";
let tests = vec![
("", None),
("foo=bar", None),
("ignition.platform.id", None),
("ignition.platform.id=", None),
("ignition.platform.id=\t", None),
("ignition.platform.id=ec2", Some("ec2".to_string())),
("ignition.platform.id=\tec2", Some("ec2".to_string())),
("ignition.platform.id=ec2\n", Some("ec2".to_string())),
("foo=bar ignition.platform.id=ec2", Some("ec2".to_string())),
("ignition.platform.id=ec2 foo=bar", Some("ec2".to_string())),
];
for (tcase, tres) in tests {
let res = find_flag_value(flagname, tcase);
assert_eq!(res, tres, "failed testcase: '{}'", tcase);
}
}
}
Loading