blob: e0670346d98cae34778702cfa5d5420b93062baa [file] [log] [blame]
// Copyright 2023 The Fuchsia 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::config;
use crate::driver::Driver;
use crate::finder::{Answer, Finder};
use crate::net::IpAddr;
use anyhow::format_err;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context, Result};
use home::home_dir;
const TESTBED_NAME: &'static str = "antlion-runner";
/// Driver for running antlion locally on an emulated or hardware testbed with
/// optional mDNS discovery when a DHCP server is not available. This is useful
/// for testing changes locally in a development environment.
pub(crate) struct LocalDriver {
target: LocalTarget,
access_point: Option<LocalAccessPoint>,
output_dir: PathBuf,
ssh_binary: PathBuf,
ffx_binary: PathBuf,
ffx_subtools_search_path: Option<PathBuf>,
}
impl LocalDriver {
pub fn new<F>(
finder: F,
device: Option<String>,
ssh_binary: PathBuf,
ssh_key: Option<PathBuf>,
ffx_binary: PathBuf,
ffx_subtools_search_path: Option<PathBuf>,
out_dir: Option<PathBuf>,
ap_ip: Option<String>,
ap_ssh_port: Option<u16>,
ap_ssh_key: Option<PathBuf>,
) -> Result<Self>
where
F: Finder,
{
let output_dir = match out_dir {
Some(p) => Ok(p),
None => std::env::current_dir().context("Failed to get current working directory"),
}?;
let target = LocalTarget::new(finder, device, ssh_key)?;
// If an access point IP has been provided, try to derive other AP-related parameters
let access_point = if let Some(ip_str) = ap_ip {
let ssh_port = ap_ssh_port.unwrap_or_else(|| {
let default_ssh_port = 22;
println!("AP IP provided without AP SSH port, assuming {default_ssh_port}");
default_ssh_port
});
let ssh_key = match ap_ssh_key {
Some(path) => Ok(path),
None => match find_ap_ssh_key() {
Ok(path) => {
println!("Using AP SSH key found at {}", path.display());
Ok(path)
}
Err(e) => Err(e),
},
}?;
Some(LocalAccessPoint {
ip: ip_str.parse::<IpAddr>().expect("Failed to parse AP IP address"),
ssh_port: Some(ssh_port),
ssh_key,
})
} else {
None
};
Ok(Self {
target,
access_point,
output_dir,
ssh_binary,
ffx_binary,
ffx_subtools_search_path,
})
}
}
fn find_ap_ssh_key() -> Result<PathBuf> {
// Look for the SSH key at some known paths
let home_dir = std::env::var("HOME").map_err(|_| {
format_err!(
"AP IP was provided, but AP SSH key not provided and could not be automatically found"
)
})?;
let home_dir = Path::new(&home_dir);
let ssh_key_search_paths =
[home_dir.join(".ssh/onhub_testing_rsa"), home_dir.join(".ssh/testing_rsa")];
for path in ssh_key_search_paths.clone() {
if path.exists() {
return Ok(path);
}
}
let ssh_key_search_paths =
ssh_key_search_paths.map(|p| p.to_string_lossy().into_owned()).join(", ");
return Err(format_err!("AP IP is provided, but AP SSH key was not provided, and not found in default locations: [{}]", ssh_key_search_paths));
}
impl Driver for LocalDriver {
fn output_path(&self) -> &Path {
self.output_dir.as_path()
}
fn config(&self) -> config::Config {
let mut access_points = vec![];
if let Some(ref ap) = self.access_point {
access_points.push(config::AccessPoint {
wan_interface: "eth0".to_string(),
ssh_config: config::SshConfig {
ssh_binary_path: self.ssh_binary.clone(),
host: ap.ip.clone(),
port: ap.ssh_port,
user: "root".to_string(),
identity_file: ap.ssh_key.clone(),
},
pdu_device: None,
attenuators: None,
});
}
config::Config {
testbeds: vec![config::Testbed {
name: TESTBED_NAME.to_string(),
controllers: config::Controllers {
fuchsia_devices: vec![config::Fuchsia {
mdns_name: self.target.name.clone(),
ip: self.target.ip.clone(),
take_bug_report_on_fail: true,
ssh_port: self.target.ssh_port.clone(),
ssh_binary_path: self.ssh_binary.clone(),
// TODO(http://b/244747218): Remove when ssh_config is refactored away
ssh_config: None,
ffx_binary_path: self.ffx_binary.clone(),
ffx_subtools_search_path: self.ffx_subtools_search_path.clone(),
ssh_priv_key: self.target.ssh_key.clone(),
pdu_device: None,
hard_reboot_on_fail: false,
}],
access_points: access_points,
..Default::default()
},
test_params: None,
}],
mobly_params: config::MoblyParams { log_path: self.output_dir.clone() },
}
}
fn teardown(&self) -> Result<()> {
println!(
"\nView full antlion logs at {}",
self.output_dir.join(TESTBED_NAME).join("latest").display()
);
Ok(())
}
}
struct LocalAccessPoint {
ip: IpAddr,
ssh_port: Option<u16>,
ssh_key: PathBuf,
}
/// LocalTargetInfo performs best-effort discovery of target information from
/// standard Fuchsia environmental variables.
struct LocalTarget {
name: String,
ip: IpAddr,
ssh_port: Option<u16>,
ssh_key: PathBuf,
}
impl LocalTarget {
fn new<F: Finder>(finder: F, device: Option<String>, ssh_key: Option<PathBuf>) -> Result<Self> {
let Answer { name, ip, ssh_port } = finder.find_device(device)?;
// TODO: Move this validation out to Args
let ssh_key = ssh_key
.or_else(|| home_dir().map(|p| p.join(".ssh/fuchsia_ed25519")))
.context("Failed to detect the private Fuchsia SSH key")?;
ensure!(
ssh_key.try_exists().with_context(|| format!(
"Failed to check existence of SSH key \"{}\"",
ssh_key.display()
))?,
"Cannot find SSH key \"{}\"",
ssh_key.display()
);
Ok(LocalTarget { name, ip, ssh_port, ssh_key })
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::generate_config_and_run;
use crate::runner::{ExitStatus, Runner};
use indoc::formatdoc;
use pretty_assertions::assert_eq;
use tempfile::{NamedTempFile, TempDir};
const FUCHSIA_NAME: &'static str = "fuchsia-1234-5678-9abc";
const FUCHSIA_ADDR: &'static str = "fe80::1%eth0";
const FUCHSIA_IP: &'static str = "fe80::1";
const FUCHSIA_IPV4: &'static str = "127.0.0.1";
const FUCHSIA_SSH_PORT: u16 = 5002;
const SCOPE_ID: &'static str = "eth0";
struct MockFinder;
impl Finder for MockFinder {
fn find_device(&self, _: Option<String>) -> Result<Answer> {
Ok(Answer {
name: FUCHSIA_NAME.to_string(),
ip: IpAddr::V6(FUCHSIA_IP.parse().unwrap(), Some(SCOPE_ID.to_string())),
ssh_port: None,
})
}
}
struct MockFinderWithSsh;
impl Finder for MockFinderWithSsh {
fn find_device(&self, _: Option<String>) -> Result<Answer> {
Ok(Answer {
name: FUCHSIA_NAME.to_string(),
ip: IpAddr::V4(FUCHSIA_IPV4.parse().unwrap()),
ssh_port: Some(FUCHSIA_SSH_PORT),
})
}
}
#[derive(Default)]
struct MockRunner {
config: std::cell::Cell<PathBuf>,
}
impl Runner for MockRunner {
fn run(&self, config: PathBuf) -> Result<ExitStatus> {
self.config.set(config);
Ok(ExitStatus::Ok)
}
}
#[test]
fn local_invalid_ssh_key() {
let ssh = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let out_dir = TempDir::new().unwrap();
assert!(LocalDriver::new(
MockFinder {},
None,
ssh.path().to_path_buf(),
Some(PathBuf::new()),
ffx.path().to_path_buf(),
None,
Some(out_dir.path().to_path_buf()),
None,
None,
None,
)
.is_err());
}
#[test]
fn local() {
let ssh = NamedTempFile::new().unwrap();
let ssh_key = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let ffx_subtools = TempDir::new().unwrap();
let out_dir = TempDir::new().unwrap();
let runner = MockRunner::default();
let driver = LocalDriver::new(
MockFinder {},
None,
ssh.path().to_path_buf(),
Some(ssh_key.path().to_path_buf()),
ffx.path().to_path_buf(),
Some(ffx_subtools.path().to_path_buf()),
Some(out_dir.path().to_path_buf()),
None,
None,
None,
)
.unwrap();
generate_config_and_run(runner, driver, None).unwrap();
let got = std::fs::read_to_string(out_dir.path().join("config.yaml")).unwrap();
let ssh_path = ssh.path().display();
let ssh_key_path = ssh_key.path().display();
let ffx_path = ffx.path().display();
let ffx_subtools_path = ffx_subtools.path().display();
let out_path = out_dir.path().display();
let want = formatdoc! {r#"
TestBeds:
- Name: {TESTBED_NAME}
Controllers:
FuchsiaDevice:
- mdns_name: {FUCHSIA_NAME}
ip: {FUCHSIA_ADDR}
take_bug_report_on_fail: true
ssh_binary_path: {ssh_path}
ffx_binary_path: {ffx_path}
ffx_subtools_search_path: {ffx_subtools_path}
ssh_priv_key: {ssh_key_path}
hard_reboot_on_fail: false
MoblyParams:
LogPath: {out_path}
"#};
assert_eq!(got, want);
}
#[test]
fn local_with_ssh_port() {
let ssh = NamedTempFile::new().unwrap();
let ssh_key = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let ffx_subtools = TempDir::new().unwrap();
let out_dir = TempDir::new().unwrap();
let runner = MockRunner::default();
let driver = LocalDriver::new(
MockFinderWithSsh {},
None,
ssh.path().to_path_buf(),
Some(ssh_key.path().to_path_buf()),
ffx.path().to_path_buf(),
Some(ffx_subtools.path().to_path_buf()),
Some(out_dir.path().to_path_buf()),
None,
None,
None,
)
.unwrap();
generate_config_and_run(runner, driver, None).unwrap();
let got = std::fs::read_to_string(out_dir.path().join("config.yaml")).unwrap();
let ssh_path = ssh.path().display();
let ssh_key_path = ssh_key.path().display();
let ffx_path = ffx.path().display();
let ffx_subtools_path = ffx_subtools.path().display();
let out_path = out_dir.path().display();
let want = formatdoc! {r#"
TestBeds:
- Name: {TESTBED_NAME}
Controllers:
FuchsiaDevice:
- mdns_name: {FUCHSIA_NAME}
ip: {FUCHSIA_IPV4}
ssh_port: {FUCHSIA_SSH_PORT}
take_bug_report_on_fail: true
ssh_binary_path: {ssh_path}
ffx_binary_path: {ffx_path}
ffx_subtools_search_path: {ffx_subtools_path}
ssh_priv_key: {ssh_key_path}
hard_reboot_on_fail: false
MoblyParams:
LogPath: {out_path}
"#};
assert_eq!(got, want);
}
#[test]
fn local_with_test_params() {
let ssh = NamedTempFile::new().unwrap();
let ssh_key = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let ffx_subtools = TempDir::new().unwrap();
let out_dir = TempDir::new().unwrap();
let runner = MockRunner::default();
let driver = LocalDriver::new(
MockFinder {},
None,
ssh.path().to_path_buf(),
Some(ssh_key.path().to_path_buf()),
ffx.path().to_path_buf(),
Some(ffx_subtools.path().to_path_buf()),
Some(out_dir.path().to_path_buf()),
None,
None,
None,
)
.unwrap();
let params_yaml = "
sl4f_sanity_test_params:
foo: bar
";
let params = serde_yaml::from_str(params_yaml).unwrap();
generate_config_and_run(runner, driver, Some(params)).unwrap();
let got = std::fs::read_to_string(out_dir.path().join("config.yaml")).unwrap();
let ssh_path = ssh.path().display().to_string();
let ssh_key_path = ssh_key.path().display().to_string();
let ffx_path = ffx.path().display().to_string();
let ffx_subtools_path = ffx_subtools.path().display();
let out_path = out_dir.path().display();
let want = formatdoc! {r#"
TestBeds:
- Name: {TESTBED_NAME}
Controllers:
FuchsiaDevice:
- mdns_name: {FUCHSIA_NAME}
ip: {FUCHSIA_ADDR}
take_bug_report_on_fail: true
ssh_binary_path: {ssh_path}
ffx_binary_path: {ffx_path}
ffx_subtools_search_path: {ffx_subtools_path}
ssh_priv_key: {ssh_key_path}
hard_reboot_on_fail: false
TestParams:
sl4f_sanity_test_params:
foo: bar
MoblyParams:
LogPath: {out_path}
"#};
assert_eq!(got, want);
}
#[test]
fn local_with_ap() {
let ssh = NamedTempFile::new().unwrap();
let ssh_key = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let ffx_subtools = TempDir::new().unwrap();
let out_dir = TempDir::new().unwrap();
let ap_ssh_key = NamedTempFile::new().unwrap();
let ap_ssh_port: u16 = 1245;
let ap_ip = "192.168.1.1".to_string();
let runner = MockRunner::default();
let driver = LocalDriver::new(
MockFinder {},
None,
ssh.path().to_path_buf(),
Some(ssh_key.path().to_path_buf()),
ffx.path().to_path_buf(),
Some(ffx_subtools.path().to_path_buf()),
Some(out_dir.path().to_path_buf()),
Some(ap_ip.clone()),
Some(ap_ssh_port),
Some(ap_ssh_key.path().to_path_buf()),
)
.unwrap();
generate_config_and_run(runner, driver, None).unwrap();
let got = std::fs::read_to_string(out_dir.path().join("config.yaml")).unwrap();
let ssh_path = ssh.path().display();
let ssh_key_path = ssh_key.path().display();
let ap_ssh_key_path = ap_ssh_key.path().display();
let ffx_path = ffx.path().display();
let ffx_subtools_path = ffx_subtools.path().display();
let out_path = out_dir.path().display();
let want = formatdoc! {r#"
TestBeds:
- Name: {TESTBED_NAME}
Controllers:
FuchsiaDevice:
- mdns_name: {FUCHSIA_NAME}
ip: {FUCHSIA_ADDR}
take_bug_report_on_fail: true
ssh_binary_path: {ssh_path}
ffx_binary_path: {ffx_path}
ffx_subtools_search_path: {ffx_subtools_path}
ssh_priv_key: {ssh_key_path}
hard_reboot_on_fail: false
AccessPoint:
- wan_interface: eth0
ssh_config:
ssh_binary_path: {ssh_path}
host: {ap_ip}
port: {ap_ssh_port}
user: root
identity_file: {ap_ssh_key_path}
MoblyParams:
LogPath: {out_path}
"#};
assert_eq!(got, want);
}
}