diff --git a/rally/cmd/envutils.py b/rally/cmd/envutils.py new file mode 100644 index 0000000000..fccff70d6c --- /dev/null +++ b/rally/cmd/envutils.py @@ -0,0 +1,31 @@ +# Copyright 2013: Mirantis Inc. +# All Rights Reserved. +# +# 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. +import os + +from rally import exceptions +from rally import fileutils + + +def default_deployment_id(): + try: + deploy_id = os.environ['RALLY_DEPLOYMENT'] + except KeyError: + fileutils.load_env_file(os.path.expanduser('~/.rally/deployment')) + try: + deploy_id = os.environ['RALLY_DEPLOYMENT'] + except KeyError: + raise exceptions.InvalidArgumentsException( + "deploy-id argument is missing") + return deploy_id diff --git a/rally/cmd/main.py b/rally/cmd/main.py index 481be4c29f..5608228afe 100644 --- a/rally/cmd/main.py +++ b/rally/cmd/main.py @@ -21,11 +21,14 @@ import json import pprint import sys +import os import prettytable from rally.cmd import cliutils +from rally.cmd import envutils from rally import db from rally import exceptions +from rally import fileutils from rally.openstack.common.gettextutils import _ from rally.orchestrator import api from rally import processing @@ -48,18 +51,19 @@ class DeploymentCommands(object): deployment = api.create_deploy(config, name) self.list(deployment_list=[deployment]) - @cliutils.args('--deploy-id', dest='deploy_id', type=str, required=True, + @cliutils.args('--deploy-id', dest='deploy_id', type=str, required=False, help='UUID of a deployment.') - def recreate(self, deploy_id): + def recreate(self, deploy_id=None): """Destroy and create an existing deployment. :param deploy_id: a UUID of the deployment """ + deploy_id = deploy_id or envutils.default_deployment_id() api.recreate_deploy(deploy_id) - @cliutils.args('--deploy-id', dest='deploy_id', type=str, required=True, + @cliutils.args('--deploy-id', dest='deploy_id', type=str, required=False, help='UUID of a deployment.') - def destroy(self, deploy_id): + def destroy(self, deploy_id=None): """Destroy the deployment. Release resources that are allocated for the deployment. The @@ -67,6 +71,7 @@ class DeploymentCommands(object): :param deploy_id: a UUID of the deployment """ + deploy_id = deploy_id or envutils.default_deployment_id() api.destroy_deploy(deploy_id) def list(self, deployment_list=None): @@ -81,23 +86,25 @@ class DeploymentCommands(object): print(table) - @cliutils.args('--deploy-id', dest='deploy_id', type=str, required=True, + @cliutils.args('--deploy-id', dest='deploy_id', type=str, required=False, help='UUID of a deployment.') - def config(self, deploy_id): + def config(self, deploy_id=None): """Print on stdout a config of the deployment in JSON format. :param deploy_id: a UUID of the deployment """ + deploy_id = deploy_id or envutils.default_deployment_id() deploy = db.deployment_get(deploy_id) print(json.dumps(deploy['config'])) - @cliutils.args('--deploy-id', dest='deploy_id', type=str, required=True, + @cliutils.args('--deploy-id', dest='deploy_id', type=str, required=False, help='UUID of a deployment.') - def endpoint(self, deploy_id): + def endpoint(self, deploy_id=None): """Print endpoint of the deployment. :param deploy_id: a UUID of the deployment """ + deploy_id = deploy_id or envutils.default_deployment_id() headers = ['auth_url', 'username', 'password', 'tenant_name'] table = prettytable.PrettyTable(headers) endpoint = db.deployment_get(deploy_id)['endpoint'] @@ -107,16 +114,17 @@ class DeploymentCommands(object): class TaskCommands(object): - @cliutils.args('--deploy-id', type=str, dest='deploy_id', required=True, + @cliutils.args('--deploy-id', type=str, dest='deploy_id', required=False, help='UUID of the deployment') @cliutils.args('--task', help='Path to the file with full configuration of task') - def start(self, deploy_id, task): + def start(self, task, deploy_id=None): """Run a benchmark task. - :param deploy_id: a UUID of a deployment :param task: a file with json configration + :param deploy_id: a UUID of a deployment """ + deploy_id = deploy_id or envutils.default_deployment_id() with open(task) as task_file: config_dict = json.load(task_file) try: @@ -285,6 +293,20 @@ class TaskCommands(object): print("Plot type '%s' not supported." % plot_type) +class UseCommands(object): + + def deployment(self, deploy_id): + """Set the RALLY_DEPLOYMENT env var to be used by all CLI commands + + :param deploy_id: a UUID of a deployment + """ + print('Using deployment : %s' % deploy_id) + if not os.path.exists(os.path.expanduser('~/.rally/')): + os.makedirs(os.path.expanduser('~/.rally/')) + expanded_path = os.path.expanduser('~/.rally/deployment') + fileutils.update_env_file(expanded_path, 'RALLY_DEPLOYMENT', deploy_id) + + def deprecated(): print("\n\n---\n\nopenstack-rally and openstack-rally-manage are " "deprecated, please use rally and rally-manage\n\n---\n\n") @@ -295,6 +317,7 @@ def main(): categories = { 'task': TaskCommands, 'deployment': DeploymentCommands, + 'use': UseCommands, } cliutils.run(sys.argv, categories) diff --git a/rally/fileutils.py b/rally/fileutils.py new file mode 100644 index 0000000000..5a7891037e --- /dev/null +++ b/rally/fileutils.py @@ -0,0 +1,72 @@ +# Copyright 2013: Mirantis Inc. +# All Rights Reserved. +# +# 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. + +import os + + +def _read_env_file(path, except_env=None): + """Read the environment variable file. + + :param path: the path of the file + :param except_env: the environment variable to avoid in the output + + :returns: the content of the original file except the line starting with + the except_env parameter + """ + output = [] + if os.path.exists(path): + with open(path, 'r') as env_file: + content = env_file.readlines() + print(content) + for line in content: + if except_env is None or \ + not line.startswith("%s=" % except_env): + output.append(line) + return output + + +def load_env_file(path): + """Load the environment variable file into os.environ. + + :param path: the path of the file + """ + if os.path.exists(path): + content = _read_env_file(path) + for line in content: + (key, sep, value) = line.partition("=") + os.environ[key] = value.rstrip() + + +def _rewrite_env_file(path, initial_content): + """Rewrite the environment variable file. + + :param path: the path of the file + :param initial_content: the original content of the file + """ + with open(path, 'w+') as env_file: + for line in initial_content: + env_file.write(line) + + +def update_env_file(path, env_key, env_value): + """Update the environment variable file. + + :param path: the path of the file + :param env_key: the key to update + :param env_value: the value of the property to update + """ + output = _read_env_file(path, env_key) + output.append('%s=%s' % (env_key, env_value)) + _rewrite_env_file(path, output) diff --git a/tests/cmd/test_envutils.py b/tests/cmd/test_envutils.py new file mode 100644 index 0000000000..66b14779d4 --- /dev/null +++ b/tests/cmd/test_envutils.py @@ -0,0 +1,38 @@ +# Copyright 2013: Mirantis Inc. +# All Rights Reserved. +# +# 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. + +import mock +import os + +from rally.cmd import envutils +from rally import exceptions +from rally.openstack.common import test + + +class EnvUtilsTestCase(test.BaseTestCase): + + @mock.patch.dict(os.environ, values={'RALLY_DEPLOYMENT': 'my_deploy_id'}, + clear=True) + def test_get_deployment_id_in_env(self): + deploy_id = envutils.default_deployment_id() + self.assertEqual('my_deploy_id', deploy_id) + + @mock.patch.dict(os.environ, values={}, clear=True) + @mock.patch('rally.cmd.envutils.fileutils.load_env_file') + def test_get_deployment_id_with_exception(self, mock_file): + self.assertRaises(exceptions.InvalidArgumentsException, + envutils.default_deployment_id) + mock_file.assert_called_once_with(os.path.expanduser( + '~/.rally/deployment')) diff --git a/tests/cmd/test_main.py b/tests/cmd/test_main.py index dad73a652a..03f7ca2eed 100644 --- a/tests/cmd/test_main.py +++ b/tests/cmd/test_main.py @@ -14,9 +14,11 @@ # under the License. import mock +import os import uuid from rally.cmd import main +from rally import exceptions from rally.openstack.common import test @@ -39,10 +41,16 @@ class TaskCommandsTestCase(test.BaseTestCase): def test_start(self, mock_api, mock_create_task, mock_task_detailed): deploy_id = str(uuid.uuid4()) - self.task.start(deploy_id, 'path_to_config.json') + self.task.start('path_to_config.json', deploy_id,) mock_api.assert_called_once_with(deploy_id, {u'some': u'json'}, task=mock_create_task.return_value) + @mock.patch('rally.cmd.main.envutils.default_deployment_id') + def test_start_no_deploy_id(self, mock_default): + mock_default.side_effect = exceptions.InvalidArgumentsException + self.assertRaises(exceptions.InvalidArgumentsException, + self.task.start, 'path_to_config.json', None) + def test_abort(self): test_uuid = str(uuid.uuid4()) with mock.patch("rally.cmd.main.api") as mock_api: @@ -129,8 +137,64 @@ class DeploymentCommandsTestCase(test.BaseTestCase): self.deployment.recreate(deploy_id) mock_recreate.assert_called_once_with(deploy_id) + @mock.patch('rally.cmd.main.envutils.default_deployment_id') + def test_recreate_no_deploy_id(self, mock_default): + mock_default.side_effect = exceptions.InvalidArgumentsException + self.assertRaises(exceptions.InvalidArgumentsException, + self.deployment.recreate, None) + @mock.patch('rally.cmd.main.api.destroy_deploy') def test_destroy(self, mock_destroy): deploy_id = str(uuid.uuid4()) self.deployment.destroy(deploy_id) mock_destroy.assert_called_once_with(deploy_id) + + @mock.patch('rally.cmd.main.envutils.default_deployment_id') + def test_destroy_no_deploy_id(self, mock_default): + mock_default.side_effect = exceptions.InvalidArgumentsException + self.assertRaises(exceptions.InvalidArgumentsException, + self.deployment.destroy, None) + + @mock.patch('rally.cmd.main.db.deployment_get') + def test_config(self, mock_deployment): + deploy_id = str(uuid.uuid4()) + value = {'config': 'config'} + mock_deployment.return_value = value + self.deployment.config(deploy_id) + mock_deployment.assert_called_once_with(deploy_id) + + @mock.patch('rally.cmd.main.envutils.default_deployment_id') + def test_config_no_deploy_id(self, mock_default): + mock_default.side_effect = exceptions.InvalidArgumentsException + self.assertRaises(exceptions.InvalidArgumentsException, + self.deployment.config, None) + + @mock.patch('rally.cmd.main.db.deployment_get') + def test_endpoint(self, mock_deployment): + deploy_id = str(uuid.uuid4()) + value = {'endpoint': {}} + mock_deployment.return_value = value + self.deployment.endpoint(deploy_id) + mock_deployment.assert_called_once_with(deploy_id) + + @mock.patch('rally.cmd.main.envutils.default_deployment_id') + def test_deploy_no_deploy_id(self, mock_default): + mock_default.side_effect = exceptions.InvalidArgumentsException + self.assertRaises(exceptions.InvalidArgumentsException, + self.deployment.endpoint, None) + + +class UseCommandsTestCase(test.BaseTestCase): + def setUp(self): + super(UseCommandsTestCase, self).setUp() + self.use = main.UseCommands() + + @mock.patch('os.path.exists') + @mock.patch('rally.cmd.main.fileutils.update_env_file') + def test_deployment(self, mock_file, mock_path): + deploy_id = str(uuid.uuid4()) + mock_path.return_value = True + self.use.deployment(deploy_id) + mock_path.assert_called_once_with(os.path.expanduser('~/.rally/')) + mock_file.assert_called_once_with(os.path.expanduser( + '~/.rally/deployment'), 'RALLY_DEPLOYMENT', deploy_id) diff --git a/tests/test_fileutils.py b/tests/test_fileutils.py new file mode 100644 index 0000000000..9a712f5d24 --- /dev/null +++ b/tests/test_fileutils.py @@ -0,0 +1,47 @@ +# Copyright 2013: Mirantis Inc. +# All Rights Reserved. +# +# 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. +import mock +import os + +from rally import fileutils +from tests import test + + +class FileUtilsTestCase(test.TestCase): + + @mock.patch('os.path.exists') + @mock.patch.dict('os.environ', values={}, clear=True) + def test_load_env_vile(self, mock_path): + file_data = ["FAKE_ENV=fake_env\n"] + mock_path.return_value = True + with mock.patch('rally.fileutils.open', mock.mock_open( + read_data=file_data), create=True) as mock_file: + mock_file.return_value.readlines.return_value = file_data + fileutils.load_env_file('path_to_file') + self.assertIn('FAKE_ENV', os.environ) + mock_file.return_value.readlines.assert_called_once_with() + + @mock.patch('os.path.exists') + def test_update_env_file(self, mock_path): + file_data = ["FAKE_ENV=old_value\n", "FAKE_ENV2=any\n"] + mock_path.return_value = True + with mock.patch('rally.fileutils.open', mock.mock_open( + read_data=file_data), create=True) as mock_file: + mock_file.return_value.readlines.return_value = file_data + fileutils.update_env_file('path_to_file', 'FAKE_ENV', 'new_value') + calls = [mock.call('FAKE_ENV2=any\n'), mock.call( + 'FAKE_ENV=new_value')] + mock_file.return_value.readlines.assert_called_once_with() + mock_file.return_value.write.assert_has_calls(calls)