diff --git a/doc/source/index.rst b/doc/source/index.rst index 10364dbb99..98b006d482 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -51,6 +51,7 @@ Command Documentation account_generator cleanup javelin + workspace ================== Indices and tables diff --git a/doc/source/workspace.rst b/doc/source/workspace.rst new file mode 100644 index 0000000000..41325b2285 --- /dev/null +++ b/doc/source/workspace.rst @@ -0,0 +1,5 @@ +----------------- +Tempest Workspace +----------------- + +.. automodule:: tempest.cmd.workspace diff --git a/releasenotes/releasenotes/notes/add-tempest-workspaces-228a2ba4690b5589.yaml b/releasenotes/releasenotes/notes/add-tempest-workspaces-228a2ba4690b5589.yaml new file mode 100644 index 0000000000..9a1cef6e15 --- /dev/null +++ b/releasenotes/releasenotes/notes/add-tempest-workspaces-228a2ba4690b5589.yaml @@ -0,0 +1,5 @@ +--- +features: + - Adds tempest workspaces command and WorkspaceManager. + This is used to have a centralized repository for managing + different tempest configurations. diff --git a/setup.cfg b/setup.cfg index 24e0214329..0bf493c76a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,7 @@ tempest.cm = run-stress = tempest.cmd.run_stress:TempestRunStress list-plugins = tempest.cmd.list_plugins:TempestListPlugins verify-config = tempest.cmd.verify_tempest_config:TempestVerifyConfig + workspace = tempest.cmd.workspace:TempestWorkspace oslo.config.opts = tempest.config = tempest.config:list_opts diff --git a/tempest/cmd/init.py b/tempest/cmd/init.py index dc28777273..77d62d34da 100644 --- a/tempest/cmd/init.py +++ b/tempest/cmd/init.py @@ -21,6 +21,8 @@ from cliff import command from oslo_log import log as logging from six import moves +from tempest.cmd.workspace import WorkspaceManager + LOG = logging.getLogger(__name__) TESTR_CONF = """[DEFAULT] @@ -89,6 +91,10 @@ class TempestInit(command.Command): action='store_true', dest='show_global_dir', help="Print the global config dir location, " "then exit") + parser.add_argument('--name', help="The workspace name", default=None) + parser.add_argument('--workspace-path', default=None, + help="The path to the workspace file, the default " + "is ~/.tempest/workspace") return parser def generate_testr_conf(self, local_path): @@ -166,6 +172,10 @@ class TempestInit(command.Command): subprocess.call(['testr', 'init'], cwd=local_dir) def take_action(self, parsed_args): + workspace_manager = WorkspaceManager(parsed_args.workspace_path) + name = parsed_args.name or parsed_args.dir.split(os.path.sep)[-1] + workspace_manager.register_new_workspace( + name, parsed_args.dir, init=True) config_dir = parsed_args.config_dir or get_tempest_default_config_dir() if parsed_args.show_global_dir: print("Global config dir is located at: %s" % config_dir) diff --git a/tempest/cmd/workspace.py b/tempest/cmd/workspace.py new file mode 100644 index 0000000000..cc82284f3f --- /dev/null +++ b/tempest/cmd/workspace.py @@ -0,0 +1,218 @@ +# Copyright 2016 Rackspace +# +# 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. + +""" +Manages Tempest workspaces + +This command is used for managing tempest workspaces + +Commands +======== + +list +---- +Outputs the name and path of all known tempest workspaces + +register +-------- +Registers a new tempest workspace via a given --name and --path + +rename +------ +Renames a tempest workspace from --old-name to --new-name + +move +---- +Changes the path of a given tempest workspace --name to --path + +remove +------ +Deletes the entry for a given tempest workspace --name + +General Options +=============== + + **--workspace_path**: Allows the user to specify a different location for the + workspace.yaml file containing the workspace definitions + instead of ~/.tempest/workspace.yaml +""" + +import os +import sys + +from cliff import command +from oslo_concurrency import lockutils +from oslo_log import log as logging +import prettytable +import yaml + +from tempest import config + +LOG = logging.getLogger(__name__) +CONF = config.CONF + + +class WorkspaceManager(object): + def __init__(self, path=None): + lockutils.get_lock_path(CONF) + self.path = path or os.path.join( + os.path.expanduser("~"), ".tempest", "workspace.yaml") + if not os.path.isdir(os.path.dirname(self.path)): + os.makedirs(self.path.rsplit(os.path.sep, 1)[0]) + self.workspaces = {} + + @lockutils.synchronized('workspaces', external=True) + def get_workspace(self, name): + """Returns the workspace that has the given name""" + self._populate() + return self.workspaces.get(name) + + @lockutils.synchronized('workspaces', external=True) + def rename_workspace(self, old_name, new_name): + self._populate() + self._name_exists(old_name) + self._workspace_name_exists(new_name) + self.workspaces[new_name] = self.workspaces.pop(old_name) + self._write_file() + + @lockutils.synchronized('workspaces', external=True) + def move_workspace(self, name, path): + self._populate() + path = os.path.abspath(os.path.expanduser(path)) + self._name_exists(name) + self._validate_path(path) + self.workspaces[name] = path + self._write_file() + + def _name_exists(self, name): + if name not in self.workspaces: + print("A workspace was not found with name: {0}".format(name)) + sys.exit(1) + + @lockutils.synchronized('workspaces', external=True) + def remove_workspace(self, name): + self._populate() + self._name_exists(name) + self.workspaces.pop(name) + self._write_file() + + @lockutils.synchronized('workspaces', external=True) + def list_workspaces(self): + self._populate() + self._validate_workspaces() + return self.workspaces + + def _workspace_name_exists(self, name): + if name in self.workspaces: + print("A workspace already exists with name: {0}.".format( + name)) + sys.exit(1) + + def _validate_path(self, path): + if not os.path.exists(path): + print("Path does not exist.") + sys.exit(1) + + @lockutils.synchronized('workspaces', external=True) + def register_new_workspace(self, name, path, init=False): + """Adds the new workspace and writes out the new workspace config""" + self._populate() + path = os.path.abspath(os.path.expanduser(path)) + # This only happens when register is called from outside of init + if not init: + self._validate_path(path) + self._workspace_name_exists(name) + self.workspaces[name] = path + self._write_file() + + def _validate_workspaces(self): + if self.workspaces is not None: + self.workspaces = {n: p for n, p in self.workspaces.items() + if os.path.exists(p)} + self._write_file() + + def _write_file(self): + with open(self.path, 'w') as f: + f.write(yaml.dump(self.workspaces)) + + def _populate(self): + if not os.path.isfile(self.path): + return + with open(self.path, 'r') as f: + self.workspaces = yaml.load(f) or {} + + +class TempestWorkspace(command.Command): + def take_action(self, parsed_args): + self.manager = WorkspaceManager(parsed_args.workspace_path) + if getattr(parsed_args, 'register', None): + self.manager.register_new_workspace( + parsed_args.name, parsed_args.path) + elif getattr(parsed_args, 'rename', None): + self.manager.rename_workspace( + parsed_args.old_name, parsed_args.new_name) + elif getattr(parsed_args, 'move', None): + self.manager.move_workspace( + parsed_args.name, parsed_args.path) + elif getattr(parsed_args, 'remove', None): + self.manager.remove_workspace( + parsed_args.name) + else: + self._print_workspaces() + sys.exit(0) + + def get_description(self): + return 'Tempest workspace actions' + + def get_parser(self, prog_name): + parser = super(TempestWorkspace, self).get_parser(prog_name) + + parser.add_argument( + '--workspace-path', required=False, default=None, + help="The path to the workspace file, the default is " + "~/.tempest/workspace.yaml") + + subparsers = parser.add_subparsers() + + list_parser = subparsers.add_parser('list') + list_parser.set_defaults(list=True) + + register_parser = subparsers.add_parser('register') + register_parser.add_argument('--name', required=True) + register_parser.add_argument('--path', required=True) + register_parser.set_defaults(register=True) + + update_parser = subparsers.add_parser('rename') + update_parser.add_argument('--old-name', required=True) + update_parser.add_argument('--new-name', required=True) + update_parser.set_defaults(rename=True) + + move_parser = subparsers.add_parser('move') + move_parser.add_argument('--name', required=True) + move_parser.add_argument('--path', required=True) + move_parser.set_defaults(move=True) + + remove_parser = subparsers.add_parser('remove') + remove_parser.add_argument('--name', required=True) + remove_parser.set_defaults(remove=True) + + return parser + + def _print_workspaces(self): + output = prettytable.PrettyTable(["Name", "Path"]) + if self.manager.list_workspaces() is not None: + for name, path in self.manager.list_workspaces().items(): + output.add_row([name, path]) + + print(output) diff --git a/tempest/tests/cmd/test_workspace.py b/tempest/tests/cmd/test_workspace.py new file mode 100644 index 0000000000..c4bd7b2ba0 --- /dev/null +++ b/tempest/tests/cmd/test_workspace.py @@ -0,0 +1,124 @@ +# Copyright 2016 Rackspace +# +# 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 +import shutil +import subprocess +import tempfile + +from tempest.cmd.workspace import WorkspaceManager +from tempest.lib.common.utils import data_utils +from tempest.tests import base + + +class TestTempestWorkspaceBase(base.TestCase): + def setUp(self): + super(TestTempestWorkspaceBase, self).setUp() + self.name = data_utils.rand_uuid() + self.path = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.path, ignore_errors=True) + store_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, store_dir, ignore_errors=True) + self.store_file = os.path.join(store_dir, 'workspace.yaml') + self.workspace_manager = WorkspaceManager(path=self.store_file) + self.workspace_manager.register_new_workspace(self.name, self.path) + + +class TestTempestWorkspace(TestTempestWorkspaceBase): + def _run_cmd_gets_return_code(self, cmd, expected): + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + return_code = process.returncode + msg = ("%s failled with:\nstdout: %s\nstderr: %s" % (' '.join(cmd), + stdout, stderr)) + self.assertEqual(return_code, expected, msg) + + def test_run_workspace_list(self): + cmd = ['tempest', 'workspace', '--workspace-path', + self.store_file, 'list'] + self._run_cmd_gets_return_code(cmd, 0) + + def test_run_workspace_register(self): + name = data_utils.rand_uuid() + path = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, path, ignore_errors=True) + cmd = ['tempest', 'workspace', '--workspace-path', self.store_file, + 'register', '--name', name, '--path', path] + self._run_cmd_gets_return_code(cmd, 0) + self.assertIsNotNone(self.workspace_manager.get_workspace(name)) + + def test_run_workspace_rename(self): + new_name = data_utils.rand_uuid() + cmd = ['tempest', 'workspace', '--workspace-path', self.store_file, + 'rename', "--old-name", self.name, '--new-name', new_name] + self._run_cmd_gets_return_code(cmd, 0) + self.assertIsNone(self.workspace_manager.get_workspace(self.name)) + self.assertIsNotNone(self.workspace_manager.get_workspace(new_name)) + + def test_run_workspace_move(self): + new_path = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, new_path, ignore_errors=True) + cmd = ['tempest', 'workspace', '--workspace-path', self.store_file, + 'move', '--name', self.name, '--path', new_path] + self._run_cmd_gets_return_code(cmd, 0) + self.assertEqual( + self.workspace_manager.get_workspace(self.name), new_path) + + def test_run_workspace_remove(self): + cmd = ['tempest', 'workspace', '--workspace-path', self.store_file, + 'remove', '--name', self.name] + self._run_cmd_gets_return_code(cmd, 0) + self.assertIsNone(self.workspace_manager.get_workspace(self.name)) + + +class TestTempestWorkspaceManager(TestTempestWorkspaceBase): + def setUp(self): + super(TestTempestWorkspaceManager, self).setUp() + self.name = data_utils.rand_uuid() + self.path = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.path, ignore_errors=True) + store_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, store_dir, ignore_errors=True) + self.store_file = os.path.join(store_dir, 'workspace.yaml') + self.workspace_manager = WorkspaceManager(path=self.store_file) + self.workspace_manager.register_new_workspace(self.name, self.path) + + def test_workspace_manager_get(self): + self.assertIsNotNone(self.workspace_manager.get_workspace(self.name)) + + def test_workspace_manager_rename(self): + new_name = data_utils.rand_uuid() + self.workspace_manager.rename_workspace(self.name, new_name) + self.assertIsNone(self.workspace_manager.get_workspace(self.name)) + self.assertIsNotNone(self.workspace_manager.get_workspace(new_name)) + + def test_workspace_manager_move(self): + new_path = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, new_path, ignore_errors=True) + self.workspace_manager.move_workspace(self.name, new_path) + self.assertEqual( + self.workspace_manager.get_workspace(self.name), new_path) + + def test_workspace_manager_remove(self): + self.workspace_manager.remove_workspace(self.name) + self.assertIsNone(self.workspace_manager.get_workspace(self.name)) + + def test_path_expansion(self): + name = data_utils.rand_uuid() + path = os.path.join("~", name) + os.makedirs(os.path.expanduser(path)) + self.addCleanup(shutil.rmtree, path, ignore_errors=True) + self.workspace_manager.register_new_workspace(name, path) + self.assertIsNotNone(self.workspace_manager.get_workspace(name))