diff --git a/doc/source/man/index.rst b/doc/source/man/index.rst index 83bbc36a040f..c8e24cf1a1e2 100644 --- a/doc/source/man/index.rst +++ b/doc/source/man/index.rst @@ -41,7 +41,7 @@ Reference nova-novncproxy nova-rootwrap nova-scheduler - nova-spicehtml5proxy - nova-xvpvncproxy nova-serialproxy - + nova-spicehtml5proxy + nova-status + nova-xvpvncproxy diff --git a/doc/source/man/nova-status.rst b/doc/source/man/nova-status.rst new file mode 100644 index 000000000000..e7b0bd31c2b6 --- /dev/null +++ b/doc/source/man/nova-status.rst @@ -0,0 +1,96 @@ +=========== +nova-status +=========== + +-------------------------------------- +CLI interface for nova status commands +-------------------------------------- + +:Author: openstack@lists.openstack.org +:Date: 2016-12-16 +:Copyright: OpenStack Foundation +:Version: 15.0.0 +:Manual section: 1 +:Manual group: cloud computing + +SYNOPSIS +======== + + nova-status [] + +DESCRIPTION +=========== + +The nova-status command provides routines for checking the status of a Nova +deployment. + +OPTIONS +======= + +The standard pattern for executing a nova-status command is:: + + nova-status [] + +Run without arguments to see a list of available command categories:: + + nova-status + +Categories are: + +* upgrade + +Detailed descriptions are below. + +You can also run with a category argument such as "upgrade" to see a list of +all commands in that category:: + + nova-status upgrade + +These sections describe the available categories and arguments for nova-status. + +Upgrade +~~~~~~~ + +``nova-status upgrade check`` + + Performs a release-specific readiness check before restarting services with + new code. This command expects to have complete configuration and access + to databases and services within a cell. For example, this check may query + the Nova API database and one or more cell databases. It may also make + requests to other services such as the Placement REST API via the Keystone + service catalog. + + **Return Codes** + + 0) All upgrade readiness checks passed successfully and there is nothing + to do. + 1) At least one check encountered an issue and requires further + investigation. This is considered a warning but the upgrade may be OK. + 2) There was an upgrade status check failure that needs to be + investigated. This should be considered something that stops an + upgrade. + + **History of Checks** + + **15.0.0 (Ocata)** + + * Checks are added for cells v2 so ``nova-status upgrade check`` should be + run *after* running the ``nova-manage cell_v2 simple_cell_setup`` + command. + * Checks are added for the Placement API such that there is an endpoint in + the Keystone service catalog, the service is running and the check can + make a successful request to the endpoint. The command also checks to + see that there are compute node resource providers checking in with the + Placement service. More information on the Placement service can be found + at: ``_ + + +SEE ALSO +======== + +* OpenStack Nova Docs: ``_ + +BUGS +==== + +* Nova bugs are managed at Launchpad: ``_ diff --git a/nova/cmd/status.py b/nova/cmd/status.py new file mode 100644 index 000000000000..7e36dd88652d --- /dev/null +++ b/nova/cmd/status.py @@ -0,0 +1,209 @@ +# Copyright 2016 IBM Corp. +# +# 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. + +""" +CLI interface for nova status commands. +""" + +from __future__ import print_function +import functools +import sys +import textwrap +import traceback + +# enum comes from the enum34 package if python < 3.4, else it's stdlib +import enum +from oslo_config import cfg +import prettytable + +from nova.cmd import common as cmd_common +import nova.conf +from nova import config +from nova.i18n import _ +from nova import version + +CONF = nova.conf.CONF + + +class UpgradeCheckCode(enum.IntEnum): + """These are the status codes for the nova-status upgrade check command + and internal check commands. + """ + + # All upgrade readiness checks passed successfully and there is + # nothing to do. + SUCCESS = 0 + + # At least one check encountered an issue and requires further + # investigation. This is considered a warning but the upgrade may be OK. + WARNING = 1 + + # There was an upgrade status check failure that needs to be + # investigated. This should be considered something that stops an upgrade. + FAILURE = 2 + + +UPGRADE_CHECK_MSG_MAP = { + UpgradeCheckCode.SUCCESS: _('Success'), + UpgradeCheckCode.WARNING: _('Warning'), + UpgradeCheckCode.FAILURE: _('Failure'), +} + + +class UpgradeCheckResult(object): + """Class used for 'nova-status upgrade check' results. + + The 'code' attribute is an UpgradeCheckCode enum. + The 'details' attribute is a translated message generally only used for + checks that result in a warning or failure code. The details should provide + information on what issue was discovered along with any remediation. + """ + + def __init__(self, code, details=None): + super(UpgradeCheckResult, self).__init__() + self.code = code + self.details = details + + +class UpgradeCommands(object): + """Commands related to upgrades. + + The subcommands here must not rely on the nova object model since they + should be able to run on n-1 data. Any queries to the database should be + done through the sqlalchemy query language directly like the database + schema migrations. + """ + + def _check_cellsv2(self): + # TODO(mriedem): perform the same checks as the 030_require_cell_setup + # API DB migration. + return UpgradeCheckResult(UpgradeCheckCode.SUCCESS) + + def _check_placement(self): + # TODO(mriedem): check to see that the placement API service is running + # and we can connect to it, and that the number of resource providers + # in the placement service is greater than or equal to the number of + # compute nodes in the database. + return UpgradeCheckResult(UpgradeCheckCode.SUCCESS) + + # The format of the check functions is to return an UpgradeCheckResult + # object with the appropriate UpgradeCheckCode and details set. If the + # check hits warnings or failures then those should be stored in the + # returned UpgradeCheckResult's "details" attribute. The summary will + # be rolled up at the end of the check() function. + _upgrade_checks = { + # Added in Ocata + _('Cells v2'): _check_cellsv2, + # Added in Ocata + _('Placement API'): _check_placement, + } + + def _get_details(self, upgrade_check_result): + if upgrade_check_result.details is not None: + # wrap the text on the details to 60 characters + return '\n'.join(textwrap.wrap(upgrade_check_result.details, 60, + subsequent_indent=' ')) + + def check(self): + """Performs checks to see if the deployment is ready for upgrade. + + These checks are expected to be run BEFORE services are restarted with + new code. These checks also require access to potentially all of the + Nova databases (nova, nova_api, nova_api_cell0) and external services + such as the placement API service. + + :returns: UpgradeCheckCode + """ + return_code = UpgradeCheckCode.SUCCESS + # This is a list if 2-item tuples for the check name and it's results. + check_results = [] + # Sort the checks by name so that we have predictable test results. + for name in sorted(self._upgrade_checks.keys()): + func = self._upgrade_checks[name] + result = func(self) + # store the result of the check for the summary table + check_results.append((name, result)) + # we want to end up with the highest level code of all checks + if result.code > return_code: + return_code = result.code + + # We're going to build a summary table that looks like: + # +----------------------------------------------------+ + # | Upgrade Check Results | + # +----------------------------------------------------+ + # | Check: Cells v2 | + # | Result: Success | + # | Details: None | + # +----------------------------------------------------+ + # | Check: Placement API | + # | Result: Failure | + # | Details: There is no placement-api endpoint in the | + # | service catalog. | + # +----------------------------------------------------+ + t = prettytable.PrettyTable([_('Upgrade Check Results')], + hrules=prettytable.ALL) + t.align = 'l' + for name, result in check_results: + cell = ( + _('Check: %(name)s\n' + 'Result: %(result)s\n' + 'Details: %(details)s') % + { + 'name': name, + 'result': UPGRADE_CHECK_MSG_MAP[result.code], + 'details': self._get_details(result), + } + ) + t.add_row([cell]) + print(t) + + return return_code + + +CATEGORIES = { + 'upgrade': UpgradeCommands, +} + + +add_command_parsers = functools.partial(cmd_common.add_command_parsers, + categories=CATEGORIES) + + +category_opt = cfg.SubCommandOpt('category', + title='Command categories', + help='Available categories', + handler=add_command_parsers) + + +def main(): + """Parse options and call the appropriate class/method.""" + CONF.register_cli_opt(category_opt) + config.parse_args(sys.argv) + + if CONF.category.name == "version": + print(version.version_string_with_package()) + return 0 + + if CONF.category.name == "bash-completion": + cmd_common.print_bash_completion(CATEGORIES) + return 0 + + try: + fn, fn_args, fn_kwargs = cmd_common.get_action_fn() + ret = fn(*fn_args, **fn_kwargs) + return(ret) + except Exception: + print(_('Error:\n%s') % traceback.format_exc()) + # This is 10 so it's not confused with the upgrade check exit codes. + return 10 diff --git a/nova/hacking/checks.py b/nova/hacking/checks.py index 963a910d30a2..71e8d1d3b68a 100644 --- a/nova/hacking/checks.py +++ b/nova/hacking/checks.py @@ -672,6 +672,7 @@ def check_config_option_in_central_place(logical_line, filename): # CLI opts are allowed to be outside of nova/conf directory 'nova/cmd/manage.py', 'nova/cmd/policy_check.py', + 'nova/cmd/status.py', # config options should not be declared in tests, but there is # another checker for it (N320) 'nova/tests', diff --git a/nova/tests/unit/cmd/test_status.py b/nova/tests/unit/cmd/test_status.py new file mode 100644 index 000000000000..7023fed23cc7 --- /dev/null +++ b/nova/tests/unit/cmd/test_status.py @@ -0,0 +1,179 @@ +# Copyright 2016 IBM Corp. +# +# 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. + +""" +Unit tests for the nova-status CLI interfaces. +""" + +import fixtures +import mock +from six.moves import StringIO + +from nova.cmd import status +import nova.conf +from nova import test + +CONF = nova.conf.CONF + + +class TestNovaStatusMain(test.NoDBTestCase): + """Tests for the basic nova-status command infrastructure.""" + + def setUp(self): + super(TestNovaStatusMain, self).setUp() + self.output = StringIO() + self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.output)) + + @mock.patch.object(status.config, 'parse_args') + @mock.patch.object(status, 'CONF') + def _check_main(self, mock_CONF, mock_parse_args, + category_name='check', expected_return_value=0): + mock_CONF.category.name = category_name + return_value = status.main() + + self.assertEqual(expected_return_value, return_value) + mock_CONF.register_cli_opt.assert_called_once_with( + status.category_opt) + + @mock.patch.object(status.version, 'version_string_with_package', + return_value="x.x.x") + def test_main_version(self, mock_version_string): + self._check_main(category_name='version') + self.assertEqual("x.x.x\n", self.output.getvalue()) + + @mock.patch.object(status.cmd_common, 'print_bash_completion') + def test_main_bash_completion(self, mock_print_bash): + self._check_main(category_name='bash-completion') + mock_print_bash.assert_called_once_with(status.CATEGORIES) + + @mock.patch.object(status.cmd_common, 'get_action_fn') + def test_main(self, mock_get_action_fn): + mock_fn = mock.Mock() + mock_fn_args = [mock.sentinel.arg] + mock_fn_kwargs = {'key': mock.sentinel.value} + mock_get_action_fn.return_value = (mock_fn, mock_fn_args, + mock_fn_kwargs) + + self._check_main(expected_return_value=mock_fn.return_value) + mock_fn.assert_called_once_with(mock.sentinel.arg, + key=mock.sentinel.value) + + @mock.patch.object(status.cmd_common, 'get_action_fn') + def test_main_error(self, mock_get_action_fn): + mock_fn = mock.Mock(side_effect=Exception('wut')) + mock_get_action_fn.return_value = (mock_fn, [], {}) + + self._check_main(expected_return_value=10) + output = self.output.getvalue() + self.assertIn('Error:', output) + # assert the traceback is in the output + self.assertIn('wut', output) + + +class TestUpgradeCheckBasic(test.NoDBTestCase): + """Tests for the nova-status upgrade check command. + + The tests in this class should just test basic logic and use mock. Real + checks which require more elaborate fixtures or the database should be done + in separate test classes as they are more or less specific to a particular + release and may be removed in a later release after they are no longer + needed. + """ + + def setUp(self): + super(TestUpgradeCheckBasic, self).setUp() + self.output = StringIO() + self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.output)) + self.cmd = status.UpgradeCommands() + + def test_check_success(self): + fake_checks = { + 'good': mock.Mock(return_value=status.UpgradeCheckResult( + status.UpgradeCheckCode.SUCCESS + )), + } + with mock.patch.object(self.cmd, '_upgrade_checks', fake_checks): + self.assertEqual(status.UpgradeCheckCode.SUCCESS, self.cmd.check()) + expected = """\ ++-----------------------+ +| Upgrade Check Results | ++-----------------------+ +| Check: good | +| Result: Success | +| Details: None | ++-----------------------+ +""" + self.assertEqual(expected, self.output.getvalue()) + + def test_check_warning(self): + fake_checks = { + 'good': mock.Mock(return_value=status.UpgradeCheckResult( + status.UpgradeCheckCode.SUCCESS + )), + 'warn': mock.Mock(return_value=status.UpgradeCheckResult( + status.UpgradeCheckCode.WARNING, 'there might be a problem' + )), + } + with mock.patch.object(self.cmd, '_upgrade_checks', fake_checks): + self.assertEqual(status.UpgradeCheckCode.WARNING, self.cmd.check()) + expected = """\ ++-----------------------------------+ +| Upgrade Check Results | ++-----------------------------------+ +| Check: good | +| Result: Success | +| Details: None | ++-----------------------------------+ +| Check: warn | +| Result: Warning | +| Details: there might be a problem | ++-----------------------------------+ +""" + self.assertEqual(expected, self.output.getvalue()) + + def test_check_failure(self): + # make the error details over 60 characters so we test the wrapping + error_details = 'go back to bed' + '!' * 60 + fake_checks = { + 'good': mock.Mock(return_value=status.UpgradeCheckResult( + status.UpgradeCheckCode.SUCCESS + )), + 'warn': mock.Mock(return_value=status.UpgradeCheckResult( + status.UpgradeCheckCode.WARNING, 'there might be a problem' + )), + 'fail': mock.Mock(return_value=status.UpgradeCheckResult( + status.UpgradeCheckCode.FAILURE, error_details + )), + } + with mock.patch.object(self.cmd, '_upgrade_checks', fake_checks): + self.assertEqual(status.UpgradeCheckCode.FAILURE, self.cmd.check()) + expected = """\ ++-----------------------------------------------------------------------+ +| Upgrade Check Results | ++-----------------------------------------------------------------------+ +| Check: fail | +| Result: Failure | +| Details: go back to bed!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | +| !!!!!!!!!!!!!! | ++-----------------------------------------------------------------------+ +| Check: good | +| Result: Success | +| Details: None | ++-----------------------------------------------------------------------+ +| Check: warn | +| Result: Warning | +| Details: there might be a problem | ++-----------------------------------------------------------------------+ +""" + self.assertEqual(expected, self.output.getvalue()) diff --git a/requirements.txt b/requirements.txt index 5ca7c3b2e741..aefd60558a19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ netaddr!=0.7.16,>=0.7.13 # BSD netifaces>=0.10.4 # MIT paramiko>=2.0 # LGPLv2.1+ Babel>=2.3.4 # BSD +enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD iso8601>=0.1.11 # MIT jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index d90bd0e452a2..9dbd5a3b7981 100644 --- a/setup.cfg +++ b/setup.cfg @@ -67,6 +67,7 @@ console_scripts = nova-scheduler = nova.cmd.scheduler:main nova-serialproxy = nova.cmd.serialproxy:main nova-spicehtml5proxy = nova.cmd.spicehtml5proxy:main + nova-status = nova.cmd.status:main nova-xvpvncproxy = nova.cmd.xvpvncproxy:main wsgi_scripts = nova-placement-api = nova.api.openstack.placement.wsgi:init_application