Simplify FuchsiaDevice config
Generate SSH configs for Fuchsia devices and remove `ssh_config` from
the FuchsiaDevice config. Provides a reproducible connection method.
Provide sensible defaults for `ssh_priv_key`, `authorized_file_loc`, and
`ffx_binary_path` by expanding the home directory and environmental
variables.
Test: python unit_tests/test_suite.py
Change-Id: Ibd1f6b2f06923da3bc8bd5aa310d1a45346f85e3
Reviewed-on: https://fuchsia-review.googlesource.com/c/antlion/+/769894
Reviewed-by: Marc Khouri <mnck@google.com>
Fuchsia-Auto-Submit: Sam Balana <sbalana@google.com>
Commit-Queue: Auto-Submit <auto-submit@fuchsia-infra.iam.gserviceaccount.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54f6914..9bc8ab7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,17 @@
## [Unreleased]
-Nothing yet!
+### Changed
+
+- All path config options in `FuchsiaDevice` expand the home directory (`~`) and
+environmental variables
+ - Used by `ssh_priv_key`, `authorized_file_loc`, and `ffx_binary_path` for
+ sensible defaults using `$FUCHSIA_DIR`
+
+### Removed
+
+- `ssh_config` from `FuchsiaDevice` config. SSH configs are generated to provide
+a reproducible connection method and ease initial setup.
[unreleased]: https://fuchsia.googlesource.com/antlion/+/refs/tags/v0.1.0..refs/heads/main
diff --git a/README.md b/README.md
index cb10088..658ef42 100644
--- a/README.md
+++ b/README.md
@@ -40,10 +40,7 @@
"testbed": [{
"name": "simple_testbed",
"FuchsiaDevice": [{
- "ip": "fuchsia-00e0-4c01-04df",
- "ssh_priv_key": "/home/fuchsia/.ssh/fuchsia_ed25519",
- "ssh_config": "/home/fuchsia/out/default/ssh-keys/ssh_config",
- "ffx_binary_path": "/home/fuchsia/out/default/host_x64/ffx"
+ "ip": "fuchsia-00e0-4c01-04df"
}]
}],
"logpath": "logs",
diff --git a/src/antlion/controllers/fuchsia_device.py b/src/antlion/controllers/fuchsia_device.py
index ceb1878..fb2c55e 100644
--- a/src/antlion/controllers/fuchsia_device.py
+++ b/src/antlion/controllers/fuchsia_device.py
@@ -20,6 +20,7 @@
import os
import re
import subprocess
+import textwrap
import time
from antlion import context
@@ -36,14 +37,12 @@
from antlion.controllers.fuchsia_lib.lib_controllers.wlan_controller import WlanController
from antlion.controllers.fuchsia_lib.lib_controllers.wlan_policy_controller import WlanPolicyController
from antlion.controllers.fuchsia_lib.package_server import PackageServer
-from antlion.controllers.fuchsia_lib.ssh import DEFAULT_SSH_PORT, DEFAULT_SSH_USER, SSHConfig, SSHProvider, FuchsiaSSHError
+from antlion.controllers.fuchsia_lib.ssh import DEFAULT_SSH_PORT, DEFAULT_SSH_PRIVATE_KEY, DEFAULT_SSH_USER, SSHConfig, SSHProvider, FuchsiaSSHError
from antlion.controllers.fuchsia_lib.utils_lib import flash
MOBLY_CONTROLLER_CONFIG_NAME = "FuchsiaDevice"
ACTS_CONTROLLER_REFERENCE_NAME = "fuchsia_devices"
-CONTROL_PATH_REPLACE_VALUE = " ControlPath /tmp/fuchsia--%r@%h:%p"
-
FUCHSIA_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
FUCHSIA_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
FUCHSIA_DEVICE_INVALID_CONFIG = ("Fuchsia device config must be either a str "
@@ -153,33 +152,60 @@
ssh_config: The ssh_config for connecting to the Fuchsia device.
"""
- def __init__(self, fd_conf_data):
- """
- Args:
- fd_conf_data: A dict of a fuchsia device configuration data
- Required keys:
- ip: IP address of fuchsia device
- optional key:
- sl4_port: Port for the sl4f web server on the fuchsia device
- (Default: 80)
- ssh_config: Location of the ssh_config file to connect to
- the fuchsia device
- (Default: None)
- ssh_port: Port for the ssh server on the fuchsia device
- (Default: 22)
- """
+ def __init__(self, fd_conf_data) -> None:
self.conf_data = fd_conf_data
if "ip" not in fd_conf_data:
raise FuchsiaDeviceError(FUCHSIA_DEVICE_NO_IP_MSG)
self.ip: str = fd_conf_data["ip"]
self.orig_ip: str = fd_conf_data["ip"]
self.sl4f_port: int = fd_conf_data.get("sl4f_port", 80)
+ self.ssh_username: str = fd_conf_data.get("ssh_username",
+ DEFAULT_SSH_USER)
self.ssh_port: int = fd_conf_data.get("ssh_port", DEFAULT_SSH_PORT)
- self.ssh_config: Optional[str] = fd_conf_data.get("ssh_config", None)
- self.ssh_priv_key: Optional[str] = fd_conf_data.get(
- "ssh_priv_key", None)
- self.authorized_file: Optional[str] = fd_conf_data.get(
- "authorized_file_loc", None)
+
+ def expand(path: str) -> str:
+ return os.path.expandvars(os.path.expanduser(path))
+
+ def path_from_config(name: str,
+ default: Optional[str] = None) -> Optional[str]:
+ path = fd_conf_data.get(name, default)
+ if not path:
+ return path
+ return expand(path)
+
+ def assert_exists(name: str, path: str) -> None:
+ if not path:
+ raise FuchsiaDeviceError(
+ f'Please specify "${name}" in your configuration file')
+ if not os.path.exists(path):
+ raise FuchsiaDeviceError(
+ f'Please specify a correct "${name}" in your configuration '
+ f'file: "{path}" does not exist')
+
+ self.specific_image: Optional[str] = path_from_config("specific_image")
+ if self.specific_image:
+ assert_exists("specific_image", self.specific_image)
+
+ # Path to a tar.gz archive with pm and amber-files, as necessary for
+ # starting a package server.
+ self.packages_archive_path: Optional[str] = path_from_config(
+ "packages_archive_path", None)
+ if self.packages_archive_path:
+ assert_exists("packages_archive_path", self.packages_archive_path)
+
+ def required_path_from_config(name: str,
+ default: Optional[str] = None) -> str:
+ path = path_from_config(name, default)
+ assert_exists(name, path)
+ return path
+
+ self.ssh_priv_key: str = required_path_from_config(
+ "ssh_priv_key", DEFAULT_SSH_PRIVATE_KEY)
+ self.authorized_file: str = required_path_from_config(
+ "authorized_file_loc", f'{self.ssh_priv_key}.pub')
+ self.ffx_binary_path: str = required_path_from_config(
+ "ffx_binary_path", "${FUCHSIA_DIR}/.jiri_root/bin/ffx")
+
self.serial_number: Optional[str] = fd_conf_data.get(
"serial_number", None)
self.device_type: Optional[str] = fd_conf_data.get("device_type", None)
@@ -189,34 +215,22 @@
self.build_number: Optional[str] = fd_conf_data.get(
"build_number", None)
self.build_type: Optional[str] = fd_conf_data.get("build_type", None)
- self.server_path: Optional[str] = fd_conf_data.get("server_path", None)
- self.specific_image: Optional[str] = fd_conf_data.get(
- "specific_image", None)
- self.ffx_binary_path: Optional[str] = fd_conf_data.get(
- "ffx_binary_path", None)
- # Path to a tar.gz archive with pm and amber-files, as necessary for
- # starting a package server.
- self.packages_archive_path: Optional[str] = fd_conf_data.get(
- "packages_archive_path", None)
+
self.mdns_name: Optional[str] = fd_conf_data.get("mdns_name", None)
- # Instead of the input ssh_config, a new config is generated with proper
- # ControlPath to the test output directory.
- output_path = context.get_current_context().get_base_output_path()
- generated_ssh_config = os.path.join(output_path,
- "ssh_config_{}".format(self.ip))
- self._set_control_path_config(self.ssh_config, generated_ssh_config)
- self.ssh_config = generated_ssh_config
-
- self.ssh_username = fd_conf_data.get("ssh_username", DEFAULT_SSH_USER)
- self.hard_reboot_on_fail = fd_conf_data.get("hard_reboot_on_fail",
- False)
- self.take_bug_report_on_fail = fd_conf_data.get(
+ self.hard_reboot_on_fail: bool = fd_conf_data.get(
+ "hard_reboot_on_fail", False)
+ self.take_bug_report_on_fail: bool = fd_conf_data.get(
"take_bug_report_on_fail", False)
self.device_pdu_config = fd_conf_data.get("PduDevice", None)
- self.config_country_code = fd_conf_data.get(
+ self.config_country_code: str = fd_conf_data.get(
'country_code', FUCHSIA_DEFAULT_COUNTRY_CODE_US).upper()
+ output_path = context.get_current_context().get_base_output_path()
+ self.ssh_config = os.path.join(output_path,
+ "ssh_config_{}".format(self.ip))
+ self._generate_ssh_config(self.ssh_config)
+
# WLAN interface info is populated inside configure_wlan
self.wlan_client_interfaces = {}
self.wlan_ap_interfaces = {}
@@ -323,10 +337,6 @@
calls.
"""
if not hasattr(self, '_ffx'):
- if not self.ffx_binary_path:
- raise FuchsiaConfigError(
- 'Must provide "ffx_binary_path: <path to FFX binary>" in the device config'
- )
if not self.mdns_name:
raise FuchsiaConfigError(
'Must provide "mdns_name: <device mDNS name>" in the device config'
@@ -343,26 +353,33 @@
self._ffx.clean_up()
del self._ffx
- def _set_control_path_config(self, old_config, new_config):
- """Given an input ssh_config, write to a new config with proper
- ControlPath values in place, if it doesn't exist already.
+ def _generate_ssh_config(self, file_path: str):
+ """Generate and write an SSH config for Fuchsia to disk.
Args:
- old_config: string, path to the input config
- new_config: string, path to store the new config
+ file_path: Path to write the generated SSH config
"""
- if os.path.isfile(new_config):
- return
+ content = textwrap.dedent(f"""\
+ Host *
+ CheckHostIP no
+ StrictHostKeyChecking no
+ ForwardAgent no
+ ForwardX11 no
+ GSSAPIDelegateCredentials no
+ UserKnownHostsFile /dev/null
+ User fuchsia
+ IdentitiesOnly yes
+ IdentityFile {self.ssh_priv_key}
+ ControlPersist yes
+ ControlMaster auto
+ ControlPath /tmp/fuchsia--%r@%h:%p
+ ServerAliveInterval 1
+ ServerAliveCountMax 1
+ LogLevel ERROR
+ """)
- ssh_config_copy = ""
-
- with open(old_config, 'r') as file:
- ssh_config_copy = re.sub('(\sControlPath\s.*)',
- CONTROL_PATH_REPLACE_VALUE,
- file.read(),
- flags=re.M)
- with open(new_config, 'w') as file:
- file.write(ssh_config_copy)
+ with open(file_path, 'w') as file:
+ file.write(content)
def init_controllers(self):
# Contains Netstack functions
@@ -874,40 +891,39 @@
Usage:
In FuchsiaDevice config, add "country_code": "<CC>"
"""
- if self.ssh_config:
- # Country code can be None, from antlion config.
- if desired_country_code:
- desired_country_code = desired_country_code.upper()
- response = self.sl4f.regulatory_region_lib.setRegion(
- desired_country_code)
- if response.get('error'):
- raise FuchsiaDeviceError(
- 'Failed to set regulatory domain. Err: %s' %
- response['error'])
+ # Country code can be None, from antlion config.
+ if desired_country_code:
+ desired_country_code = desired_country_code.upper()
+ response = self.sl4f.regulatory_region_lib.setRegion(
+ desired_country_code)
+ if response.get('error'):
+ raise FuchsiaDeviceError(
+ 'Failed to set regulatory domain. Err: %s' %
+ response['error'])
- phy_list_response = self.sl4f.wlan_lib.wlanPhyIdList()
- if phy_list_response.get('error'):
- raise FuchsiaDeviceError(
- f'Failed to get phy list. Err: {response["error"]}')
- phy_list = phy_list_response.get('result')
- if not phy_list:
- raise FuchsiaDeviceError('No phy available in phy list')
- phy_id = phy_list[0]
+ phy_list_response = self.sl4f.wlan_lib.wlanPhyIdList()
+ if phy_list_response.get('error'):
+ raise FuchsiaDeviceError(
+ f'Failed to get phy list. Err: {response["error"]}')
+ phy_list = phy_list_response.get('result')
+ if not phy_list:
+ raise FuchsiaDeviceError('No phy available in phy list')
+ phy_id = phy_list[0]
- end_time = time.time() + FUCHSIA_COUNTRY_CODE_TIMEOUT
- while time.time() < end_time:
- ascii_cc = self.sl4f.wlan_lib.wlanGetCountry(phy_id).get(
- 'result')
- # Convert ascii_cc to string, then compare
- if ascii_cc and (''.join(chr(c) for c in ascii_cc).upper()
- == desired_country_code):
- self.log.debug('Country code successfully set to %s.' %
- desired_country_code)
- return
- self.log.debug('Country code not yet updated. Retrying.')
- time.sleep(1)
- raise FuchsiaDeviceError('Country code never updated to %s' %
- desired_country_code)
+ end_time = time.time() + FUCHSIA_COUNTRY_CODE_TIMEOUT
+ while time.time() < end_time:
+ ascii_cc = self.sl4f.wlan_lib.wlanGetCountry(phy_id).get(
+ 'result')
+ # Convert ascii_cc to string, then compare
+ if ascii_cc and (''.join(chr(c) for c in ascii_cc).upper()
+ == desired_country_code):
+ self.log.debug('Country code successfully set to %s.' %
+ desired_country_code)
+ return
+ self.log.debug('Country code not yet updated. Retrying.')
+ time.sleep(1)
+ raise FuchsiaDeviceError('Country code never updated to %s' %
+ desired_country_code)
def stop_services(self):
"""Stops the ffx daemon and deletes SL4F property."""
@@ -930,11 +946,6 @@
times in one test. Epoch time when the test started. If not
specified, the current time will be used.
"""
- if not self.ssh_config:
- self.log.warn(
- 'Skipping take_bug_report because ssh_config is not specified')
- return
-
if test_name:
self.log.info(
f"Taking snapshot of {self.mdns_name} for {test_name}")
diff --git a/src/antlion/controllers/fuchsia_lib/ssh.py b/src/antlion/controllers/fuchsia_lib/ssh.py
index fd32d99..ec8f762 100644
--- a/src/antlion/controllers/fuchsia_lib/ssh.py
+++ b/src/antlion/controllers/fuchsia_lib/ssh.py
@@ -25,6 +25,7 @@
DEFAULT_SSH_USER: str = "fuchsia"
DEFAULT_SSH_PORT: int = 22
+DEFAULT_SSH_PRIVATE_KEY: str = "~/.ssh/fuchsia_ed25519"
DEFAULT_SSH_TIMEOUT_SEC: int = 60
DEFAULT_SSH_CONNECT_TIMEOUT_SEC: int = 30
DEFAULT_SSH_SERVER_ALIVE_INTERVAL: int = 30
diff --git a/unit_tests/acts_utils_test.py b/unit_tests/acts_utils_test.py
index bf55ec6..1c8b21f 100755
--- a/unit_tests/acts_utils_test.py
+++ b/unit_tests/acts_utils_test.py
@@ -280,8 +280,8 @@
values returned from each individual callable in the order passed in.
"""
ret_values = utils.run_concurrent_actions_no_raise(
- lambda: self.function_returns_passed_in_arg(
- 'ARG1'), lambda: self.function_returns_passed_in_arg('ARG2'),
+ lambda: self.function_returns_passed_in_arg('ARG1'),
+ lambda: self.function_returns_passed_in_arg('ARG2'),
lambda: self.function_returns_passed_in_arg('ARG3'))
self.assertEqual(len(ret_values), 3)
@@ -312,8 +312,8 @@
"""
ret_values = utils.run_concurrent_actions(
- lambda: self.function_returns_passed_in_arg(
- 'ARG1'), lambda: self.function_returns_passed_in_arg('ARG2'),
+ lambda: self.function_returns_passed_in_arg('ARG1'),
+ lambda: self.function_returns_passed_in_arg('ARG2'),
lambda: self.function_returns_passed_in_arg('ARG3'))
self.assertEqual(len(ret_values), 3)
@@ -370,6 +370,7 @@
class IpAddressUtilTest(unittest.TestCase):
+
def test_positive_ipv4_normal_address(self):
ip_address = "192.168.1.123"
self.assertTrue(utils.is_valid_ipv4_address(ip_address))
@@ -432,7 +433,8 @@
self.assertEqual(utils.get_interface_ip_addresses(job, 'wlan1'),
CORRECT_EMPTY_IP_LIST)
- @mock.patch('antlion.controllers.utils_lib.ssh.connection.SshConnection.run')
+ @mock.patch(
+ 'antlion.controllers.utils_lib.ssh.connection.SshConnection.run')
def test_ssh_get_interface_ip_addresses_full(self, ssh_mock):
ssh_mock.side_effect = [
job.Result(stdout=bytes(MOCK_ENO1_IP_ADDRESSES, 'utf-8'),
@@ -442,7 +444,8 @@
utils.get_interface_ip_addresses(SshConnection('mock_settings'),
'eno1'), CORRECT_FULL_IP_LIST)
- @mock.patch('antlion.controllers.utils_lib.ssh.connection.SshConnection.run')
+ @mock.patch(
+ 'antlion.controllers.utils_lib.ssh.connection.SshConnection.run')
def test_ssh_get_interface_ip_addresses_empty(self, ssh_mock):
ssh_mock.side_effect = [
job.Result(stdout=bytes(MOCK_WLAN1_IP_ADDRESSES, 'utf-8'),
@@ -483,14 +486,14 @@
@mock.patch(
'antlion.controllers.fuchsia_lib.sl4f.SL4F._verify_sl4f_connection')
@mock.patch('antlion.controllers.fuchsia_device.'
- 'FuchsiaDevice._set_control_path_config')
+ 'FuchsiaDevice._generate_ssh_config')
@mock.patch('antlion.controllers.'
'fuchsia_lib.netstack.netstack_lib.'
'FuchsiaNetstackLib.netstackListInterfaces')
def test_fuchsia_get_interface_ip_addresses_full(
- self, list_interfaces_mock, control_path_mock,
- verify_sl4f_conn_mock, ssh_run_mock, wait_for_port_mock,
- ffx_mock, sl4f_mock):
+ self, list_interfaces_mock, generate_ssh_config_mock,
+ verify_sl4f_conn_mock, ssh_run_mock, wait_for_port_mock, ffx_mock,
+ sl4f_mock):
# Configure the log path which is required by ACTS logger.
logging.log_path = '/tmp/unit_test_garbage'
@@ -519,14 +522,14 @@
@mock.patch(
'antlion.controllers.fuchsia_lib.sl4f.SL4F._verify_sl4f_connection')
@mock.patch('antlion.controllers.fuchsia_device.'
- 'FuchsiaDevice._set_control_path_config')
+ 'FuchsiaDevice._generate_ssh_config')
@mock.patch('antlion.controllers.'
'fuchsia_lib.netstack.netstack_lib.'
'FuchsiaNetstackLib.netstackListInterfaces')
def test_fuchsia_get_interface_ip_addresses_empty(
- self, list_interfaces_mock, control_path_mock,
- verify_sl4f_conn_mock, ssh_run_mock, wait_for_port_mock,
- ffx_mock, sl4f_mock):
+ self, list_interfaces_mock, generate_ssh_config_mock,
+ verify_sl4f_conn_mock, ssh_run_mock, wait_for_port_mock, ffx_mock,
+ sl4f_mock):
# Configure the log path which is required by ACTS logger.
logging.log_path = '/tmp/unit_test_garbage'
@@ -548,7 +551,9 @@
class GetDeviceTest(unittest.TestCase):
+
class TestDevice:
+
def __init__(self, id, device_type=None) -> None:
self.id = id
if device_type: