| # Copyright 2019 Google LLC |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Tests for lab_cli.""" |
| import atexit |
| import copy |
| import datetime |
| import json |
| import os |
| import shutil |
| import tempfile |
| from unittest import mock |
| |
| from absl.testing import absltest |
| from absl.testing import parameterized |
| from tradefed_cluster.configs import lab_config |
| |
| |
| from multitest_transport.cli import lab_cli |
| from multitest_transport.cli import unittest_util |
| |
| |
| _PROJECT = 'aproject' |
| _SERVICE_ACCOUNT_EMAIL = 'as@gsa.com' |
| _KEY_CREATE_TIME = '2021-04-15T23:13:34Z' |
| _SERVICE_ACCOUNT_KEY_ID = 'akey_id' |
| _SERVICE_ACCOUNT_KEY = { |
| 'type': 'service_account', |
| 'project_id': _PROJECT, |
| 'private_key_id': _SERVICE_ACCOUNT_KEY_ID, |
| 'private_key': 'aprivate_key', |
| 'client_email': _SERVICE_ACCOUNT_EMAIL, |
| 'client_id': '1234', |
| 'auth_uri': 'https://accounts.google.com/o/oauth2/auth', |
| 'token_uri': 'https://oauth2.googleapis.com/token', |
| 'auth_provider_x509_cert_url': 'https://www.googleapis.com/oauth2/v1/certs', |
| 'client_x509_cert_url': 'url' |
| } |
| _SERVICE_ACCOUNT_KEY_INFO = { |
| 'name': (f'projects/{_PROJECT}/serviceAccounts/' |
| f'{_SERVICE_ACCOUNT_EMAIL}/keys/{_SERVICE_ACCOUNT_KEY_ID}'), |
| 'validAfterTime': _KEY_CREATE_TIME, |
| 'validBeforeTime': '2021-07-14T23:13:34Z', |
| 'keyAlgorithm': 'KEY_ALG_RSA_2048', |
| 'keyOrigin': 'GOOGLE_PROVIDED', |
| 'keyType': 'USER_MANAGED' |
| } |
| |
| |
| class LabCliTest(parameterized.TestCase): |
| |
| def setUp(self): |
| super(LabCliTest, self).setUp() |
| self.arg_parser = lab_cli.CreateParser() |
| self.tmp_root = tempfile.mkdtemp() |
| |
| def tearDown(self): |
| shutil.rmtree(self.tmp_root) |
| super(LabCliTest, self).tearDown() |
| |
| def testCreateParser(self): |
| args = self.arg_parser.parse_args(['version']) |
| self.assertEqual('version', args.action) |
| |
| def testCreateParser_withLoggingArgs(self): |
| args = self.arg_parser.parse_args([ |
| '-v', '--log_file', 'file.log', 'version']) |
| self.assertEqual('version', args.action) |
| self.assertTrue(args.verbose) |
| self.assertEqual('file.log', args.log_file) |
| |
| @mock.patch.object(tempfile, 'mkdtemp') |
| def testSetupMTTBinary(self, mock_create_temp): |
| tmp_folder = os.path.join(self.tmp_root, 'mtt_lab_extracted') |
| os.mkdir(tmp_folder) |
| mock_create_temp.return_value = tmp_folder |
| mtt_lab_path = os.path.join(self.tmp_root, 'mtt_lab') |
| unittest_util.CreateZipFile( |
| mtt_lab_path, |
| [unittest_util.File( |
| filename='mtt_binary', content='this is a mtt binary')]) |
| |
| args = mock.MagicMock(cli_path=mtt_lab_path) |
| host = mock.MagicMock( |
| context=mock.MagicMock(user='testuser')) |
| lab_cli._SetupMTTBinary(args, host) |
| |
| host.context.assert_has_calls([ |
| mock.call.CopyFile( |
| os.path.join(tmp_folder, 'mtt_binary'), |
| '/tmp/testuser/mtt'), |
| mock.call.Run(['chmod', '+x', '/tmp/testuser/mtt'])]) |
| self.assertEqual('Setting up MTT Binary', host.execution_state) |
| |
| @mock.patch.object(tempfile, 'NamedTemporaryFile') |
| def testSetupHostConfig(self, mock_make_tmpfile): |
| mock_tmpfile = mock.MagicMock() |
| mock_tmpfile.name = '/local/config.yaml' |
| mock_make_tmpfile.return_value = mock_tmpfile |
| host = mock.MagicMock(context=mock.MagicMock(user='testuser')) |
| lab_cli._SetupHostConfig(host) |
| |
| host.config.Save.assert_called_once_with('/local/config.yaml') |
| host.context.CopyFile.assert_called_once_with( |
| '/local/config.yaml', '/tmp/testuser/mtt_host_config.yaml') |
| mock_tmpfile.close.assert_called_once_with() |
| self.assertEqual('Setting up host config', host.execution_state) |
| |
| @parameterized.named_parameters( |
| ('$ mtt_lab start --ask_sudo_password --sudo_user user', |
| True, 'user', True), |
| ('$ mtt_lab start --sudo_user user', False, 'user', True), |
| ('$ mtt_lab start --ask_sudo_password', True, None, True), |
| ('$ mtt_lab start', False, None, False)) |
| @mock.patch.object(lab_cli, '_SetupMTTBinary') |
| @mock.patch.object(tempfile, 'NamedTemporaryFile') |
| @mock.patch.object(lab_config.HostConfig, 'Save') |
| def testStart( |
| self, ask_sudo_password, sudo_user, use_sudo, mock_save, |
| mock_make_tmpfile, mock_setup): |
| mock_tmpfile = mock.MagicMock() |
| mock_tmpfile.name = '/local/config.yaml' |
| mock_make_tmpfile.return_value = mock_tmpfile |
| args = mock.MagicMock( |
| service_account_json_key_path='/path/to/keyfile.json', |
| ask_sudo_password=ask_sudo_password, |
| sudo_user=sudo_user, |
| verbose=False, |
| very_verbose=False) |
| host = mock.MagicMock( |
| context=mock.MagicMock(user='testuser'), |
| config=lab_config.CreateHostConfig( |
| service_account_json_key_path='/path/to/keyfile.json')) |
| lab_cli.Start(args, host) |
| |
| host.context.assert_has_calls([ |
| mock.call.CopyFile('/path/to/keyfile.json', |
| '/tmp/testuser/keyfile/key.json'), |
| mock.call.CopyFile('/local/config.yaml', |
| '/tmp/testuser/mtt_host_config.yaml'), |
| mock.call.Run(['/tmp/testuser/mtt', '--no_check_update', 'start', |
| '/tmp/testuser/mtt_host_config.yaml'], |
| sudo=use_sudo)]) |
| self.assertEqual('/tmp/testuser/keyfile/key.json', |
| host.config.service_account_json_key_path) |
| mock_save.assert_called_once_with('/local/config.yaml') |
| mock_setup.assert_called_once_with(args, host) |
| mock_tmpfile.close.assert_called_once_with() |
| self.assertEqual('Running start', host.execution_state) |
| |
| @mock.patch.object(lab_cli, '_SetupMTTBinary') |
| @mock.patch.object(tempfile, 'NamedTemporaryFile') |
| @mock.patch.object(lab_config.HostConfig, 'Save') |
| def testStart_noServiceAccountKey( |
| self, mock_save, mock_make_tmpfile, mock_setup): |
| mock_tmpfile = mock.MagicMock() |
| mock_tmpfile.name = '/local/config.yaml' |
| mock_make_tmpfile.return_value = mock_tmpfile |
| args = mock.MagicMock( |
| service_account_json_key_path=None, |
| sudo_user=None, |
| ask_sudo_password=False, |
| verbose=False, |
| very_verbose=False) |
| host = mock.MagicMock( |
| context=mock.MagicMock(user='testuser'), |
| config=lab_config.CreateHostConfig( |
| service_account_json_key_path=None)) |
| lab_cli.Start(args, host) |
| |
| host.context.assert_has_calls([ |
| mock.call.CopyFile( |
| '/local/config.yaml', '/tmp/testuser/mtt_host_config.yaml'), |
| mock.call.Run(['/tmp/testuser/mtt', '--no_check_update', 'start', |
| '/tmp/testuser/mtt_host_config.yaml'], sudo=False)]) |
| mock_save.assert_called_once_with('/local/config.yaml') |
| mock_setup.assert_called_once_with(args, host) |
| mock_tmpfile.close.assert_called_once_with() |
| |
| @parameterized.named_parameters( |
| ('$ mtt_lab update --ask_sudo_password --sudo_user user', |
| True, 'user', True), |
| ('$ mtt_lab update --sudo_user user', False, 'user', True), |
| ('$ mtt_lab update --ask_sudo_password', True, None, True), |
| ('$ mtt_lab update', False, None, False)) |
| @mock.patch.object(lab_cli, '_SetupMTTBinary') |
| @mock.patch.object(tempfile, 'NamedTemporaryFile') |
| @mock.patch.object(lab_config.HostConfig, 'Save') |
| def testUpdate( |
| self, ask_sudo_password, sudo_user, use_sudo, mock_save, |
| mock_make_tmpfile, mock_setup): |
| mock_tmpfile = mock.MagicMock() |
| mock_tmpfile.name = '/local/config.yaml' |
| mock_make_tmpfile.return_value = mock_tmpfile |
| args = mock.MagicMock( |
| service_account_json_key_path='/path/to/keyfile.json', |
| ask_sudo_password=ask_sudo_password, |
| sudo_user=sudo_user, |
| verbose=False, |
| very_verbose=False) |
| host = mock.MagicMock( |
| context=mock.MagicMock(user='testuser'), |
| config=lab_config.CreateHostConfig( |
| service_account_json_key_path='/path/to/keyfile.json')) |
| lab_cli.Update(args, host) |
| |
| host.context.assert_has_calls([ |
| mock.call.CopyFile('/path/to/keyfile.json', |
| '/tmp/testuser/keyfile/key.json'), |
| mock.call.CopyFile('/local/config.yaml', |
| '/tmp/testuser/mtt_host_config.yaml'), |
| mock.call.Run(['/tmp/testuser/mtt', '--no_check_update', 'update', |
| '/tmp/testuser/mtt_host_config.yaml'], |
| sudo=use_sudo)]) |
| self.assertEqual('/tmp/testuser/keyfile/key.json', |
| host.config.service_account_json_key_path) |
| mock_save.assert_called_once_with('/local/config.yaml') |
| mock_setup.assert_called_once_with(args, host) |
| mock_tmpfile.close.assert_called_once_with() |
| self.assertEqual('Running update', host.execution_state) |
| |
| @parameterized.named_parameters( |
| ('$ mtt_lab restart --ask_sudo_password --sudo_user user', |
| True, 'user', True), |
| ('$ mtt_lab restart --sudo_user user', False, 'user', True), |
| ('$ mtt_lab restart --ask_sudo_password', True, None, True), |
| ('$ mtt_lab restart', False, None, False)) |
| @mock.patch.object(lab_cli, '_SetupMTTBinary') |
| @mock.patch.object(tempfile, 'NamedTemporaryFile') |
| @mock.patch.object(lab_config.HostConfig, 'Save') |
| def testRestart( |
| self, ask_sudo_password, sudo_user, use_sudo, mock_save, |
| mock_make_tmpfile, mock_setup): |
| mock_tmpfile = mock.MagicMock() |
| mock_tmpfile.name = '/local/config.yaml' |
| mock_make_tmpfile.return_value = mock_tmpfile |
| args = mock.MagicMock( |
| service_account_json_key_path='/path/to/keyfile.json', |
| ask_sudo_password=ask_sudo_password, |
| sudo_user=sudo_user, |
| verbose=False, |
| very_verbose=False) |
| host = mock.MagicMock( |
| context=mock.MagicMock(user='testuser'), |
| config=lab_config.CreateHostConfig( |
| service_account_json_key_path='/path/to/keyfile.json')) |
| lab_cli.Restart(args, host) |
| |
| host.context.assert_has_calls([ |
| mock.call.CopyFile('/path/to/keyfile.json', |
| '/tmp/testuser/keyfile/key.json'), |
| mock.call.CopyFile('/local/config.yaml', |
| '/tmp/testuser/mtt_host_config.yaml'), |
| mock.call.Run(['/tmp/testuser/mtt', '--no_check_update', 'restart', |
| '/tmp/testuser/mtt_host_config.yaml'], |
| sudo=use_sudo)]) |
| self.assertEqual('/tmp/testuser/keyfile/key.json', |
| host.config.service_account_json_key_path) |
| mock_save.assert_called_once_with('/local/config.yaml') |
| mock_setup.assert_called_once_with(args, host) |
| mock_tmpfile.close.assert_called_once_with() |
| self.assertEqual('Running restart', host.execution_state) |
| |
| @parameterized.named_parameters( |
| ('$ mtt_lab stop --ask_sudo_password --sudo_user user', |
| True, 'user', True), |
| ('$ mtt_lab stop --sudo_user user', False, 'user', True), |
| ('$ mtt_lab stop --ask_sudo_password', True, None, True), |
| ('$ mtt_lab stop', False, None, False)) |
| @mock.patch.object(lab_cli, '_SetupMTTBinary') |
| def testStop(self, ask_sudo_password, sudo_user, use_sudo, mock_setup): |
| args = mock.MagicMock( |
| ask_sudo_password=ask_sudo_password, |
| sudo_user=sudo_user, |
| verbose=False, |
| very_verbose=False) |
| host = mock.MagicMock(context=mock.MagicMock(user='testuser')) |
| lab_cli.Stop(args, host) |
| |
| host.context.Run.assert_called_once_with( |
| ['/tmp/testuser/mtt', '--no_check_update', 'stop'], |
| sudo=use_sudo) |
| mock_setup.assert_called_once_with(args, host) |
| self.assertEqual('Running stop', host.execution_state) |
| |
| @parameterized.named_parameters( |
| ('run_cmd_with_sudo_pwd_and_user', True, 'user', True), |
| ('run_cmd_with_sudo_user', False, 'user', True), |
| ('run_cmd_with_sudo_pwd', True, 'user', True), |
| ('run_cmd_without_sudo', False, None, False)) |
| def testRunCmd(self, ask_sudo_password, sudo_user, use_sudo): |
| args = mock.MagicMock( |
| ask_sudo_password=ask_sudo_password, |
| sudo_user=sudo_user, |
| cmd='run a command line') |
| host = mock.MagicMock() |
| lab_cli.RunCmd(args, host) |
| host.context.Run.assert_called_once_with( |
| ['run', 'a', 'command', 'line'], sudo=use_sudo) |
| self.assertEqual('Running cmd', host.execution_state) |
| |
| def testCreateLabCommandArgParser(self): |
| parser = lab_cli._CreateLabCommandArgParser() |
| args = parser.parse_args(['lab_config.yaml']) |
| self.assertEqual('lab_config.yaml', args.lab_config_path) |
| self.assertEmpty(args.hosts_or_clusters) |
| |
| def testCreateLabCommandArgParser_withHostOrCluster(self): |
| parser = lab_cli._CreateLabCommandArgParser() |
| args = parser.parse_args(['lab_config.yaml', 'host1', 'cluster1']) |
| self.assertEqual('lab_config.yaml', args.lab_config_path) |
| self.assertEqual(['host1', 'cluster1'], args.hosts_or_clusters) |
| |
| @parameterized.named_parameters( |
| ('no_verbose', [], ['--no_check_update']), |
| ('verbose', ['-v'], ['-v', '--no_check_update']), |
| ('very_verbose', ['-vv'], ['-vv', '--no_check_update']), |
| ('both', ['-v', '-vv'], ['-vv', '--no_check_update'])) |
| def testBuildBaseMTTCmd(self, verbose_flag, res_flag): |
| parser = lab_cli.cli_util.CreateLoggingArgParser() |
| args = parser.parse_args(verbose_flag) |
| host = mock.MagicMock(context=mock.MagicMock(user='testuser')) |
| cmd = lab_cli._BuildBaseMTTCmd(args, host) |
| self.assertEqual( |
| ['/tmp/testuser/mtt'] + res_flag, |
| cmd) |
| |
| def testSetupServiceAccountKey(self): |
| args = mock.MagicMock(service_account_json_key_path=None) |
| host = mock.MagicMock( |
| context=mock.MagicMock(user='testuser'), |
| config=lab_config.CreateHostConfig( |
| service_account_json_key_path='/path/to/keyfile.json')) |
| lab_cli._SetupServiceAccountKey(args, host) |
| |
| host.context.CopyFile.assert_called_once_with( |
| '/path/to/keyfile.json', '/tmp/testuser/keyfile/key.json') |
| self.assertEqual('/tmp/testuser/keyfile/key.json', |
| host.config.service_account_json_key_path) |
| self.assertEqual('Setting up service account key', host.execution_state) |
| |
| @mock.patch.object(tempfile, 'NamedTemporaryFile') |
| @mock.patch.object(lab_cli.google_auth_util, 'CreateKey') |
| @mock.patch.object(lab_cli.google_auth_util, 'UpdateSecret') |
| @mock.patch.object(lab_cli.google_auth_util, 'GetServiceAccountKeyInfo') |
| @mock.patch.object(lab_cli.google_auth_util, 'GetSecret') |
| @mock.patch.object(lab_cli.google_auth_util, 'CanCreateKey') |
| @mock.patch.object(lab_cli.google_auth_util, 'CanUpdateSecret') |
| @mock.patch.object(lab_cli.google_auth_util, 'DisableSecretVersions') |
| @mock.patch.object(atexit, 'register') |
| def testGetServiceAccountKeyFilePath( |
| self, mock_register, mock_disable_versions, mock_can_update_secret, |
| mock_can_create_key, mock_get_secret, mock_get_key_info, |
| mock_update_secret, mock_create_key, mock_make_tmpfile): |
| mock_tmpfile = mock.MagicMock() |
| mock_tmpfile.name = '/local/sa_key.json' |
| mock_make_tmpfile.return_value = mock_tmpfile |
| mock_get_secret.return_value = json.dumps(_SERVICE_ACCOUNT_KEY).encode() |
| key_info = copy.deepcopy(_SERVICE_ACCOUNT_KEY_INFO) |
| key_info['validAfterTime'] = ( |
| datetime.datetime.now(tz=datetime.timezone.utc) |
| - datetime.timedelta(days=3)).isoformat() |
| mock_get_key_info.return_value = key_info |
| |
| lab_cli._GetServiceAccountKeyFilePath('secret_project', 'sa_key') |
| |
| mock_get_key_info.assert_called_once_with( |
| _SERVICE_ACCOUNT_EMAIL, _SERVICE_ACCOUNT_KEY_ID) |
| self.assertTrue(mock_can_create_key.called) |
| self.assertTrue(mock_can_update_secret.called) |
| self.assertFalse(mock_create_key.called) |
| self.assertFalse(mock_update_secret.called) |
| self.assertFalse(mock_disable_versions.called) |
| mock_tmpfile.write.assert_called_once_with( |
| json.dumps(_SERVICE_ACCOUNT_KEY).encode()) |
| mock_tmpfile.flush.assert_called_once_with() |
| mock_get_secret.assert_called_once_with('secret_project', 'sa_key') |
| mock_register.assert_called_once_with(mock_tmpfile.close) |
| |
| @mock.patch.object(tempfile, 'NamedTemporaryFile') |
| @mock.patch.object(lab_cli.google_auth_util, 'CreateKey') |
| @mock.patch.object(lab_cli.google_auth_util, 'UpdateSecret') |
| @mock.patch.object(lab_cli.google_auth_util, 'GetServiceAccountKeyInfo') |
| @mock.patch.object(lab_cli.google_auth_util, 'GetSecret') |
| @mock.patch.object(lab_cli.google_auth_util, 'CanCreateKey') |
| @mock.patch.object(lab_cli.google_auth_util, 'CanUpdateSecret') |
| @mock.patch.object(lab_cli.google_auth_util, 'DisableSecretVersions') |
| @mock.patch.object(atexit, 'register') |
| def testGetServiceAccountKeyFilePath_renew( |
| self, mock_register, mock_disable_versions, mock_can_update_secret, |
| mock_can_create_key, mock_get_secret, mock_get_key_info, |
| mock_update_secret, mock_create_key, mock_make_tmpfile): |
| mock_tmpfile = mock.MagicMock() |
| mock_tmpfile.name = '/local/sa_key.json' |
| mock_make_tmpfile.return_value = mock_tmpfile |
| mock_get_secret.return_value = json.dumps(_SERVICE_ACCOUNT_KEY).encode() |
| key_info = copy.deepcopy(_SERVICE_ACCOUNT_KEY_INFO) |
| key_info['validAfterTime'] = ( |
| datetime.datetime.now(tz=datetime.timezone.utc) |
| - datetime.timedelta(days=20)).isoformat() |
| mock_get_key_info.return_value = key_info |
| mock_can_create_key.return_value = True |
| mock_can_update_secret.return_value = True |
| new_key = copy.deepcopy(_SERVICE_ACCOUNT_KEY) |
| new_key['private_key_id'] = 'new_key_id' |
| mock_create_key.return_value = new_key |
| mock_update_secret.return_value = 'new_version' |
| |
| lab_cli._GetServiceAccountKeyFilePath('secret_project', 'sa_key') |
| |
| mock_get_key_info.assert_called_once_with( |
| _SERVICE_ACCOUNT_EMAIL, _SERVICE_ACCOUNT_KEY_ID) |
| mock_can_update_secret.assert_called_once_with('secret_project', 'sa_key') |
| mock_can_create_key.assert_called_once_with(_SERVICE_ACCOUNT_EMAIL) |
| mock_create_key.assert_called_once_with(_SERVICE_ACCOUNT_EMAIL) |
| mock_update_secret.assert_called_once_with( |
| 'secret_project', 'sa_key', json.dumps(new_key).encode()) |
| mock_disable_versions.assert_called_once_with( |
| 'secret_project', 'sa_key', exclude_versions=['new_version'], |
| days_before=28) |
| mock_tmpfile.write.assert_called_once_with(json.dumps(new_key).encode()) |
| mock_tmpfile.flush.assert_called_once_with() |
| mock_get_secret.assert_called_once_with('secret_project', 'sa_key') |
| mock_register.assert_called_once_with(mock_tmpfile.close) |
| |
| @mock.patch.object(lab_cli.google_auth_util, 'GetServiceAccountKeyInfo') |
| def testShouldRenewServiceAccountKey_noNeedToRenew(self, mock_get_key_info): |
| key_info = copy.deepcopy(_SERVICE_ACCOUNT_KEY_INFO) |
| key_info['validAfterTime'] = ( |
| datetime.datetime.now(tz=datetime.timezone.utc) |
| - datetime.timedelta(days=3)).isoformat() |
| mock_get_key_info.return_value = key_info |
| |
| shoud_renew = lab_cli._ShouldRenewServiceAccountKey(_SERVICE_ACCOUNT_KEY) |
| |
| self.assertFalse(shoud_renew) |
| mock_get_key_info.assert_called_once_with( |
| _SERVICE_ACCOUNT_EMAIL, _SERVICE_ACCOUNT_KEY_ID) |
| |
| @mock.patch.object(lab_cli.google_auth_util, 'GetServiceAccountKeyInfo') |
| def testShouldRenewServiceAccountKey_shouldRenew(self, mock_get_key_info): |
| key_info = copy.deepcopy(_SERVICE_ACCOUNT_KEY_INFO) |
| key_info['validAfterTime'] = ( |
| datetime.datetime.now(tz=datetime.timezone.utc) |
| - datetime.timedelta(days=20)).isoformat() |
| mock_get_key_info.return_value = key_info |
| |
| shoud_renew = lab_cli._ShouldRenewServiceAccountKey(_SERVICE_ACCOUNT_KEY) |
| |
| self.assertTrue(shoud_renew) |
| mock_get_key_info.assert_called_once_with( |
| _SERVICE_ACCOUNT_EMAIL, _SERVICE_ACCOUNT_KEY_ID) |
| |
| |
| if __name__ == '__main__': |
| absltest.main() |