Unverified Commit 977b1a73 authored by Yifan Xiong's avatar Yifan Xiong Committed by GitHub
Browse files

CLI - Refine CLI handlers (#68)

* use absolute path of input file
* parse registry uri from image
* merge common parts for arguments processing
parent af6eb004
...@@ -146,6 +146,7 @@ def run(self): ...@@ -146,6 +146,7 @@ def run(self):
'mypy>=0.800', 'mypy>=0.800',
'pydocstyle>=5.1.1', 'pydocstyle>=5.1.1',
'pytest-cov>=2.11.1', 'pytest-cov>=2.11.1',
'pytest-subtests>=0.4.0',
'pytest>=6.2.2', 'pytest>=6.2.2',
'vcrpy>=4.1.1', 'vcrpy>=4.1.1',
'yapf>=0.30.0', 'yapf>=0.30.0',
......
...@@ -21,42 +21,92 @@ def check_argument_file(name, file): ...@@ -21,42 +21,92 @@ def check_argument_file(name, file):
name (str): argument name. name (str): argument name.
file (str): file path. file (str): file path.
Returns:
str: Absolute file path if it exists.
Raises: Raises:
CLIError: If file does not exist. CLIError: If file does not exist.
""" """
if file and not Path(file).exists(): if file:
if not Path(file).exists():
raise CLIError('{} {} does not exist.'.format(name, file)) raise CLIError('{} {} does not exist.'.format(name, file))
return str(Path(file).resolve())
return file
def version_command_handler(): def split_docker_domain(name):
"""Print the current SuperBench tool version. """Split Docker image name to domain and remainder part.
Ported from https://github.com/distribution/distribution/blob/v2.7.1/reference/normalize.go#L62-L76.
Args:
name (str): Docker image name.
Returns: Returns:
str: current SuperBench tool version. str: Docker registry domain.
str: Remainder part.
""" """
return superbench.__version__ legacy_default_domain = 'index.docker.io'
default_domain = 'docker.io'
i = name.find('/')
domain, remainder = '', ''
if i == -1 or ('.' not in name[:i] and ':' not in name[:i] and name[:i] != 'localhost'):
domain, remainder = default_domain, name
else:
domain, remainder = name[:i], name[i + 1:]
if domain == legacy_default_domain:
domain = default_domain
if domain == default_domain and '/' not in remainder:
remainder = 'library/{}'.format(remainder)
return domain, remainder
def deploy_command_handler(
docker_image, def process_config_arguments(config_file=None, config_override=None):
"""Process configuration arguments.
Args:
config_file (str, optional): Path to SuperBench config file. Defaults to None.
config_override (str, optional): Extra arguments to override config_file,
following [Hydra syntax](https://hydra.cc/docs/advanced/override_grammar/basic). Defaults to None.
Returns:
DictConfig: SuperBench config object.
str: Dir for output.
Raises:
CLIError: If input arguments are invalid.
"""
config_file = check_argument_file('config_file', config_file)
# SuperBench config
sb_config = get_sb_config(config_file)
if config_override:
sb_config_from_override = OmegaConf.from_dotlist(config_override)
sb_config = OmegaConf.merge(sb_config, sb_config_from_override)
# Create output directory
output_dir = create_output_dir()
return sb_config, output_dir
def process_runner_arguments(
docker_image='superbench/superbench',
docker_username=None, docker_username=None,
docker_password=None, docker_password=None,
host_file=None, host_file=None,
host_list=None, host_list=None,
host_username=None, host_username=None,
host_password=None, host_password=None,
private_key=None private_key=None,
config_file=None,
config_override=None
): ):
"""Deploy the SuperBench environments to all given nodes. """Process runner related arguments.
Deploy SuperBench environments on all nodes, including:
1. check drivers
2. install required system dependencies
3. install Docker and container runtime
4. pull Docker image
Args: Args:
docker_image (str): Docker image URI. docker_image (str, optional): Docker image URI. Defaults to superbench/superbench:latest.
docker_username (str, optional): Docker registry username if authentication is needed. Defaults to None. docker_username (str, optional): Docker registry username if authentication is needed. Defaults to None.
docker_password (str, optional): Docker registry password if authentication is needed. Defaults to None. docker_password (str, optional): Docker registry password if authentication is needed. Defaults to None.
host_file (str, optional): Path to Ansible inventory host file. Defaults to None. host_file (str, optional): Path to Ansible inventory host file. Defaults to None.
...@@ -64,6 +114,15 @@ def deploy_command_handler( ...@@ -64,6 +114,15 @@ def deploy_command_handler(
host_username (str, optional): Host username if needed. Defaults to None. host_username (str, optional): Host username if needed. Defaults to None.
host_password (str, optional): Host password or key passphase if needed. Defaults to None. host_password (str, optional): Host password or key passphase if needed. Defaults to None.
private_key (str, optional): Path to private key if needed. Defaults to None. private_key (str, optional): Path to private key if needed. Defaults to None.
config_file (str, optional): Path to SuperBench config file. Defaults to None.
config_override (str, optional): Extra arguments to override config_file,
following [Hydra syntax](https://hydra.cc/docs/advanced/override_grammar/basic). Defaults to None.
Returns:
DictConfig: SuperBench config object.
DictConfig: Docker config object.
DictConfig: Ansible config object.
str: Dir for output.
Raises: Raises:
CLIError: If input arguments are invalid. CLIError: If input arguments are invalid.
...@@ -72,21 +131,47 @@ def deploy_command_handler( ...@@ -72,21 +131,47 @@ def deploy_command_handler(
raise CLIError('Must specify both docker_username and docker_password if authentication is needed.') raise CLIError('Must specify both docker_username and docker_password if authentication is needed.')
if not (host_file or host_list): if not (host_file or host_list):
raise CLIError('Must specify one of host_file or host_list.') raise CLIError('Must specify one of host_file or host_list.')
check_argument_file('host_file', host_file) host_file = check_argument_file('host_file', host_file)
check_argument_file('private_key', private_key) private_key = check_argument_file('private_key', private_key)
raise NotImplementedError # Docker config
docker_config = OmegaConf.create(
{
'image': docker_image,
'username': docker_username,
'password': docker_password,
'registry': split_docker_domain(docker_image)[0],
}
)
# Ansible config
ansible_config = OmegaConf.create(
{
'host_file': host_file,
'host_list': host_list,
'host_username': host_username,
'host_password': host_password,
'private_key': private_key,
}
)
sb_config, output_dir = process_config_arguments(config_file=config_file, config_override=config_override)
def exec_command_handler( return docker_config, ansible_config, sb_config, output_dir
docker_image=None, docker_username=None, docker_password=None, config_file=None, config_override=None
):
def version_command_handler():
"""Print the current SuperBench tool version.
Returns:
str: current SuperBench tool version.
"""
return superbench.__version__
def exec_command_handler(config_file=None, config_override=None):
"""Run the SuperBench benchmarks locally. """Run the SuperBench benchmarks locally.
Args: Args:
docker_image (str, optional): Docker image URI.
docker_username (str, optional): Docker registry username if authentication is needed. Defaults to None.
docker_password (str, optional): Docker registry password if authentication is needed. Defaults to None.
config_file (str, optional): Path to SuperBench config file. Defaults to None. config_file (str, optional): Path to SuperBench config file. Defaults to None.
config_override (str, optional): Extra arguments to override config_file, config_override (str, optional): Extra arguments to override config_file,
following [Hydra syntax](https://hydra.cc/docs/advanced/override_grammar/basic). Defaults to None. following [Hydra syntax](https://hydra.cc/docs/advanced/override_grammar/basic). Defaults to None.
...@@ -94,29 +179,60 @@ def exec_command_handler( ...@@ -94,29 +179,60 @@ def exec_command_handler(
Raises: Raises:
CLIError: If input arguments are invalid. CLIError: If input arguments are invalid.
""" """
if bool(docker_username) != bool(docker_password): sb_config, output_dir = process_config_arguments(config_file=config_file, config_override=config_override)
raise CLIError('Must specify both docker_username and docker_password if authentication is needed.')
check_argument_file('config_file', config_file)
# Docker config executor = SuperBenchExecutor(sb_config, output_dir)
docker_config = OmegaConf.create() executor.exec()
for key in ['image', 'username', 'password']:
docker_config[key] = eval('docker_{}'.format(key))
# SuperBench config
sb_config = get_sb_config(config_file)
if config_override:
sb_config_from_override = OmegaConf.from_dotlist(config_override)
sb_config = OmegaConf.merge(sb_config, sb_config_from_override)
# Create output directory
output_dir = create_output_dir()
executor = SuperBenchExecutor(sb_config, docker_config, output_dir) def deploy_command_handler(
executor.exec() docker_image='superbench/superbench',
docker_username=None,
docker_password=None,
host_file=None,
host_list=None,
host_username=None,
host_password=None,
private_key=None
):
"""Deploy the SuperBench environments to all given nodes.
Deploy SuperBench environments on all nodes, including:
1. check drivers
2. install required system dependencies
3. install Docker and container runtime
4. pull Docker image
Args:
docker_image (str, optional): Docker image URI. Defaults to superbench/superbench:latest.
docker_username (str, optional): Docker registry username if authentication is needed. Defaults to None.
docker_password (str, optional): Docker registry password if authentication is needed. Defaults to None.
host_file (str, optional): Path to Ansible inventory host file. Defaults to None.
host_list (str, optional): Comma separated host list. Defaults to None.
host_username (str, optional): Host username if needed. Defaults to None.
host_password (str, optional): Host password or key passphase if needed. Defaults to None.
private_key (str, optional): Path to private key if needed. Defaults to None.
Raises:
CLIError: If input arguments are invalid.
"""
docker_config, ansible_config, sb_config, output_dir = process_runner_arguments(
docker_image=docker_image,
docker_username=docker_username,
docker_password=docker_password,
host_file=host_file,
host_list=host_list,
host_username=host_username,
host_password=host_password,
private_key=private_key,
)
SuperBenchRunner(sb_config, docker_config, ansible_config, output_dir)
raise NotImplementedError
def run_command_handler( def run_command_handler(
docker_image, docker_image='superbench/superbench',
docker_username=None, docker_username=None,
docker_password=None, docker_password=None,
host_file=None, host_file=None,
...@@ -132,7 +248,7 @@ def run_command_handler( ...@@ -132,7 +248,7 @@ def run_command_handler(
Run all benchmarks on given nodes. Run all benchmarks on given nodes.
Args: Args:
docker_image (str): Docker image URI. docker_image (str, optional): Docker image URI. Defaults to superbench/superbench:latest.
docker_username (str, optional): Docker registry username if authentication is needed. Defaults to None. docker_username (str, optional): Docker registry username if authentication is needed. Defaults to None.
docker_password (str, optional): Docker registry password if authentication is needed. Defaults to None. docker_password (str, optional): Docker registry password if authentication is needed. Defaults to None.
host_file (str, optional): Path to Ansible inventory host file. Defaults to None. host_file (str, optional): Path to Ansible inventory host file. Defaults to None.
...@@ -147,30 +263,18 @@ def run_command_handler( ...@@ -147,30 +263,18 @@ def run_command_handler(
Raises: Raises:
CLIError: If input arguments are invalid. CLIError: If input arguments are invalid.
""" """
if bool(docker_username) != bool(docker_password): docker_config, ansible_config, sb_config, output_dir = process_runner_arguments(
raise CLIError('Must specify both docker_username and docker_password if authentication is needed.') docker_image=docker_image,
if not (host_file or host_list): docker_username=docker_username,
raise CLIError('Must specify one of host_file or host_list.') docker_password=docker_password,
check_argument_file('host_file', host_file) host_file=host_file,
check_argument_file('private_key', private_key) host_list=host_list,
check_argument_file('config_file', config_file) host_username=host_username,
host_password=host_password,
# Docker config private_key=private_key,
docker_config = OmegaConf.create() config_file=config_file,
for key in ['image', 'username', 'password']: config_override=config_override,
docker_config[key] = eval('docker_{}'.format(key)) )
# Ansible config
ansible_config = OmegaConf.create()
for key in ['file', 'list', 'username', 'password']:
ansible_config['host_{}'.format(key)] = eval('host_{}'.format(key))
# SuperBench config
sb_config = get_sb_config(config_file)
if config_override:
sb_config_from_override = OmegaConf.from_dotlist(config_override)
sb_config = OmegaConf.merge(sb_config, sb_config_from_override)
# Create output directory
output_dir = create_output_dir()
runner = SuperBenchRunner(sb_config, docker_config, ansible_config, output_dir) runner = SuperBenchRunner(sb_config, docker_config, ansible_config, output_dir)
runner.run() runner.run()
...@@ -18,7 +18,7 @@ def create_output_dir(): ...@@ -18,7 +18,7 @@ def create_output_dir():
str: Output directory name. str: Output directory name.
""" """
output_name = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') output_name = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
output_path = Path('.', 'outputs', output_name) output_path = Path('.', 'outputs', output_name).resolve()
output_path.mkdir(mode=0o755, parents=True, exist_ok=True) output_path.mkdir(mode=0o755, parents=True, exist_ok=True)
return str(output_path) return str(output_path)
......
...@@ -13,16 +13,14 @@ ...@@ -13,16 +13,14 @@
class SuperBenchExecutor(): class SuperBenchExecutor():
"""SuperBench executor class.""" """SuperBench executor class."""
def __init__(self, sb_config, docker_config, output_dir): def __init__(self, sb_config, output_dir):
"""Initilize. """Initilize.
Args: Args:
sb_config (DictConfig): SuperBench config object. sb_config (DictConfig): SuperBench config object.
docker_config (DictConfig): Docker config object.
output_dir (str): Dir for output. output_dir (str): Dir for output.
""" """
self._sb_config = sb_config self._sb_config = sb_config
self._docker_config = docker_config
self._output_dir = output_dir self._output_dir = output_dir
self.__set_logger('sb-exec.log') self.__set_logger('sb-exec.log')
......
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
"""CLI handler test."""
import unittest
import superbench.cli._handler as cli_handler
class CLIHandlerTestCase(unittest.TestCase):
"""A class for CLI handler test cases."""
def test_split_docker_domain(self):
"""Test split_docker_domain function which splits Docker image name to domain and remainder part.
Test cases are ported from
https://github.com/distribution/distribution/blob/v2.7.1/reference/normalize_test.go#L468-L528.
"""
test_cases = [
{
'input': 'test.com/foo',
'domain': 'test.com',
'name': 'foo',
},
{
'input': 'test_com/foo',
'domain': 'docker.io',
'name': 'test_com/foo',
},
{
'input': 'docker/migrator',
'domain': 'docker.io',
'name': 'docker/migrator',
},
{
'input': 'test.com:8080/foo',
'domain': 'test.com:8080',
'name': 'foo',
},
{
'input': 'test-com:8080/foo',
'domain': 'test-com:8080',
'name': 'foo',
},
{
'input': 'foo',
'domain': 'docker.io',
'name': 'library/foo',
},
{
'input': 'xn--n3h.com/foo',
'domain': 'xn--n3h.com',
'name': 'foo',
},
{
'input': 'xn--n3h.com:18080/foo',
'domain': 'xn--n3h.com:18080',
'name': 'foo',
},
{
'input': 'docker.io/foo',
'domain': 'docker.io',
'name': 'library/foo',
},
{
'input': 'docker.io/library/foo',
'domain': 'docker.io',
'name': 'library/foo',
},
{
'input': 'docker.io/library/foo/bar',
'domain': 'docker.io',
'name': 'library/foo/bar',
},
]
for test_case in test_cases:
with self.subTest(msg='Testing with case', test_case=test_case):
domain, name = cli_handler.split_docker_domain(test_case['input'])
self.assertEqual(domain, test_case['domain'])
self.assertEqual(name, test_case['name'])
...@@ -53,13 +53,7 @@ def test_sb_version(self): ...@@ -53,13 +53,7 @@ def test_sb_version(self):
def test_sb_deploy(self): def test_sb_deploy(self):
"""Test sb deploy.""" """Test sb deploy."""
self.cmd('sb deploy --docker-image test:cuda11.1 --host-list localhost', expect_failure=True) self.cmd('sb deploy --host-list localhost', expect_failure=True)
@capture_system_exit
def test_sb_deploy_no_docker_image(self):
"""Test sb deploy, no --docker-image argument, should fail."""
self.cmd('sb deploy', expect_failure=True)
self.assertIn('sb deploy: error: the following arguments are required: --docker-image', self.stderr)
def test_sb_exec(self): def test_sb_exec(self):
"""Test sb exec.""" """Test sb exec."""
...@@ -67,13 +61,12 @@ def test_sb_exec(self): ...@@ -67,13 +61,12 @@ def test_sb_exec(self):
def test_sb_run(self): def test_sb_run(self):
"""Test sb run.""" """Test sb run."""
self.cmd('sb run --docker-image test:cuda11.1 --host-list localhost', checks=[NoneCheck()]) self.cmd('sb run --host-list localhost', checks=[NoneCheck()])
@capture_system_exit def test_sb_run_no_docker_auth(self):
def test_sb_run_no_docker_image(self): """Test sb run, only --docker-username argument, should fail."""
"""Test sb run, no --docker-image argument, should fail.""" result = self.cmd('sb run --docker-username test-user', expect_failure=True)
self.cmd('sb run', expect_failure=True) self.assertEqual(result.exit_code, 1)
self.assertIn('sb run: error: the following arguments are required: --docker-image', self.stderr)
def test_sb_run_no_host(self): def test_sb_run_no_host(self):
"""Test sb run, no --host-file or --host-list, should fail.""" """Test sb run, no --host-file or --host-list, should fail."""
...@@ -82,5 +75,5 @@ def test_sb_run_no_host(self): ...@@ -82,5 +75,5 @@ def test_sb_run_no_host(self):
def test_sb_run_nonexist_host_file(self): def test_sb_run_nonexist_host_file(self):
"""Test sb run, --host-file does not exist, should fail.""" """Test sb run, --host-file does not exist, should fail."""
result = self.cmd('sb run --docker-image test:cuda11.1 --host-file ./nonexist.yaml', expect_failure=True) result = self.cmd('sb run --host-file ./nonexist.yaml', expect_failure=True)
self.assertEqual(result.exit_code, 1) self.assertEqual(result.exit_code, 1)
...@@ -25,7 +25,7 @@ def setUp(self): ...@@ -25,7 +25,7 @@ def setUp(self):
self.default_config = OmegaConf.load(str(default_config_file)) self.default_config = OmegaConf.load(str(default_config_file))
self.output_dir = tempfile.mkdtemp() self.output_dir = tempfile.mkdtemp()
self.executor = SuperBenchExecutor(self.default_config, None, self.output_dir) self.executor = SuperBenchExecutor(self.default_config, self.output_dir)
def tearDown(self): def tearDown(self):
"""Hook method for deconstructing the test fixture after testing it.""" """Hook method for deconstructing the test fixture after testing it."""
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment