8cddd243bf
This change introduces a new nova-status check to ensure a machine type has been recorded for each instance within an environment. nova-status will fail with a warning when instances are found, directing the operator to use the previously added nova-manage list_unset and update commands to set a machine type for these instances. The logic for this check comes entirely from the list_unset command. It is noted in the warning output that this can be ignored if no libvirt or HyperV based computes are present in the environment as hw_machine_type is only used by these two virt drivers at present. blueprint: libvirt-default-machine-type Change-Id: Ic3ae48c57e61c4e45883fbae1328a448be025953
392 lines
18 KiB
Python
392 lines
18 KiB
Python
# 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.
|
|
"""
|
|
|
|
import functools
|
|
import sys
|
|
import traceback
|
|
|
|
from keystoneauth1 import exceptions as ks_exc
|
|
from oslo_config import cfg
|
|
from oslo_upgradecheck import common_checks
|
|
from oslo_upgradecheck import upgradecheck
|
|
import pkg_resources
|
|
from sqlalchemy import func as sqlfunc
|
|
from sqlalchemy import MetaData, Table, select
|
|
|
|
from nova.cmd import common as cmd_common
|
|
import nova.conf
|
|
from nova import config
|
|
from nova import context as nova_context
|
|
from nova.db.sqlalchemy import api as db_session
|
|
from nova import exception
|
|
from nova.i18n import _
|
|
from nova.objects import cell_mapping as cell_mapping_obj
|
|
# NOTE(lyarwood): The following are imported as machine_type_utils expects them
|
|
# to be registered under nova.objects when called via _check_machine_type_set
|
|
from nova.objects import image_meta as image_meta_obj # noqa: F401
|
|
from nova.objects import instance as instance_obj # noqa: F401
|
|
from nova import policy
|
|
from nova import utils
|
|
from nova import version
|
|
from nova.virt.libvirt import machine_type_utils
|
|
from nova.volume import cinder
|
|
|
|
CONF = nova.conf.CONF
|
|
|
|
# NOTE(efried): 1.35 is required by nova-scheduler to support the root_required
|
|
# queryparam to make GET /allocation_candidates require that a trait be present
|
|
# on the root provider, irrespective of how the request groups are specified.
|
|
# NOTE: If you bump this version, remember to update the history
|
|
# section in the nova-status man page (doc/source/cli/nova-status).
|
|
MIN_PLACEMENT_MICROVERSION = "1.35"
|
|
|
|
# NOTE(mriedem): 3.44 is needed to work with volume attachment records which
|
|
# are required for supporting multi-attach capable volumes.
|
|
MIN_CINDER_MICROVERSION = '3.44'
|
|
|
|
|
|
class UpgradeCommands(upgradecheck.UpgradeCommands):
|
|
"""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 _count_compute_nodes(self, context=None):
|
|
"""Returns the number of compute nodes in the cell database."""
|
|
# NOTE(mriedem): This does not filter based on the service status
|
|
# because a disabled nova-compute service could still be reporting
|
|
# inventory info to the placement service. There could be an outside
|
|
# chance that there are compute node records in the database for
|
|
# disabled nova-compute services that aren't yet upgraded to Ocata or
|
|
# the nova-compute service was deleted and the service isn't actually
|
|
# running on the compute host but the operator hasn't cleaned up the
|
|
# compute_nodes entry in the database yet. We consider those edge cases
|
|
# here and the worst case scenario is we give a warning that there are
|
|
# more compute nodes than resource providers. We can tighten this up
|
|
# later if needed, for example by not including compute nodes that
|
|
# don't have a corresponding nova-compute service in the services
|
|
# table, or by only counting compute nodes with a service version of at
|
|
# least 15 which was the highest service version when Newton was
|
|
# released.
|
|
meta = MetaData(bind=db_session.get_engine(context=context))
|
|
compute_nodes = Table('compute_nodes', meta, autoload=True)
|
|
return select([sqlfunc.count()]).select_from(compute_nodes).where(
|
|
compute_nodes.c.deleted == 0).scalar()
|
|
|
|
def _check_cellsv2(self):
|
|
"""Checks to see if cells v2 has been setup.
|
|
|
|
These are the same checks performed in the 030_require_cell_setup API
|
|
DB migration except we expect this to be run AFTER the
|
|
nova-manage cell_v2 simple_cell_setup command, which would create the
|
|
cell and host mappings and sync the cell0 database schema, so we don't
|
|
check for flavors at all because you could create those after doing
|
|
this on an initial install. This also has to be careful about checking
|
|
for compute nodes if there are no host mappings on a fresh install.
|
|
"""
|
|
meta = MetaData()
|
|
meta.bind = db_session.get_api_engine()
|
|
|
|
cell_mappings = self._get_cell_mappings()
|
|
count = len(cell_mappings)
|
|
# Two mappings are required at a minimum, cell0 and your first cell
|
|
if count < 2:
|
|
msg = _('There needs to be at least two cell mappings, one for '
|
|
'cell0 and one for your first cell. Run command '
|
|
'\'nova-manage cell_v2 simple_cell_setup\' and then '
|
|
'retry.')
|
|
return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
|
|
|
|
cell0 = any(mapping.is_cell0() for mapping in cell_mappings)
|
|
if not cell0:
|
|
msg = _('No cell0 mapping found. Run command '
|
|
'\'nova-manage cell_v2 simple_cell_setup\' and then '
|
|
'retry.')
|
|
return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
|
|
|
|
host_mappings = Table('host_mappings', meta, autoload=True)
|
|
count = select([sqlfunc.count()]).select_from(host_mappings).scalar()
|
|
if count == 0:
|
|
# This may be a fresh install in which case there may not be any
|
|
# compute_nodes in the cell database if the nova-compute service
|
|
# hasn't started yet to create those records. So let's query the
|
|
# cell database for compute_nodes records and if we find at least
|
|
# one it's a failure.
|
|
num_computes = self._count_compute_nodes()
|
|
if num_computes > 0:
|
|
msg = _('No host mappings found but there are compute nodes. '
|
|
'Run command \'nova-manage cell_v2 '
|
|
'simple_cell_setup\' and then retry.')
|
|
return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
|
|
|
|
msg = _('No host mappings or compute nodes were found. Remember '
|
|
'to run command \'nova-manage cell_v2 discover_hosts\' '
|
|
'when new compute hosts are deployed.')
|
|
return upgradecheck.Result(upgradecheck.Code.SUCCESS, msg)
|
|
|
|
return upgradecheck.Result(upgradecheck.Code.SUCCESS)
|
|
|
|
@staticmethod
|
|
def _placement_get(path):
|
|
"""Do an HTTP get call against placement engine.
|
|
|
|
This is in a dedicated method to make it easier for unit
|
|
testing purposes.
|
|
|
|
"""
|
|
client = utils.get_ksa_adapter('placement')
|
|
return client.get(path, raise_exc=True).json()
|
|
|
|
def _check_placement(self):
|
|
"""Checks to see if the placement API is ready for scheduling.
|
|
|
|
Checks to see that the placement API service is registered in the
|
|
service catalog and that we can make requests against it.
|
|
"""
|
|
try:
|
|
# TODO(efried): Use ksa's version filtering in _placement_get
|
|
versions = self._placement_get("/")
|
|
max_version = pkg_resources.parse_version(
|
|
versions["versions"][0]["max_version"])
|
|
needs_version = pkg_resources.parse_version(
|
|
MIN_PLACEMENT_MICROVERSION)
|
|
if max_version < needs_version:
|
|
msg = (_('Placement API version %(needed)s needed, '
|
|
'you have %(current)s.') %
|
|
{'needed': needs_version, 'current': max_version})
|
|
return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
|
|
except ks_exc.MissingAuthPlugin:
|
|
msg = _('No credentials specified for placement API in nova.conf.')
|
|
return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
|
|
except ks_exc.Unauthorized:
|
|
msg = _('Placement service credentials do not work.')
|
|
return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
|
|
except ks_exc.EndpointNotFound:
|
|
msg = _('Placement API endpoint not found.')
|
|
return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
|
|
except ks_exc.DiscoveryFailure:
|
|
msg = _('Discovery for placement API URI failed.')
|
|
return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
|
|
except ks_exc.NotFound:
|
|
msg = _('Placement API does not seem to be running.')
|
|
return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
|
|
|
|
return upgradecheck.Result(upgradecheck.Code.SUCCESS)
|
|
|
|
@staticmethod
|
|
def _get_cell_mappings():
|
|
"""Queries the API database for cell mappings.
|
|
|
|
.. note:: This method is unique in that it queries the database using
|
|
CellMappingList.get_all() rather than a direct query using
|
|
the sqlalchemy models. This is because
|
|
CellMapping.database_connection can be a template and the
|
|
object takes care of formatting the URL. We cannot use
|
|
RowProxy objects from sqlalchemy because we cannot set the
|
|
formatted 'database_connection' value back on those objects
|
|
(they are read-only).
|
|
|
|
:returns: list of nova.objects.CellMapping objects
|
|
"""
|
|
ctxt = nova_context.get_admin_context()
|
|
cell_mappings = cell_mapping_obj.CellMappingList.get_all(ctxt)
|
|
return cell_mappings
|
|
|
|
def _check_cinder(self):
|
|
"""Checks to see that the cinder API is available at a given minimum
|
|
microversion.
|
|
"""
|
|
# Check to see if nova is even configured for Cinder yet (fresh install
|
|
# or maybe not using Cinder at all).
|
|
if CONF.cinder.auth_type is None:
|
|
return upgradecheck.Result(upgradecheck.Code.SUCCESS)
|
|
|
|
try:
|
|
# TODO(mriedem): Eventually use get_ksa_adapter here when it
|
|
# supports cinder.
|
|
cinder.is_microversion_supported(
|
|
nova_context.get_admin_context(), MIN_CINDER_MICROVERSION)
|
|
except exception.CinderAPIVersionNotAvailable:
|
|
return upgradecheck.Result(
|
|
upgradecheck.Code.FAILURE,
|
|
_('Cinder API %s or greater is required. Deploy at least '
|
|
'Cinder 12.0.0 (Queens).') % MIN_CINDER_MICROVERSION)
|
|
except Exception as ex:
|
|
# Anything else trying to connect, like bad config, is out of our
|
|
# hands so just return a warning.
|
|
return upgradecheck.Result(
|
|
upgradecheck.Code.WARNING,
|
|
_('Unable to determine Cinder API version due to error: %s') %
|
|
str(ex))
|
|
return upgradecheck.Result(upgradecheck.Code.SUCCESS)
|
|
|
|
def _check_policy(self):
|
|
"""Checks to see if policy file is overwritten with the new
|
|
defaults.
|
|
"""
|
|
msg = _("Your policy file contains rules which examine token scope, "
|
|
"which may be due to generation with the new defaults. "
|
|
"If that is done intentionally to migrate to the new rule "
|
|
"format, then you are required to enable the flag "
|
|
"'oslo_policy.enforce_scope=True' and educate end users on "
|
|
"how to request scoped tokens from Keystone. Another easy "
|
|
"and recommended way for you to achieve the same is via two "
|
|
"flags, 'oslo_policy.enforce_scope=True' and "
|
|
"'oslo_policy.enforce_new_defaults=True' and avoid "
|
|
"overwriting the file. Please refer to this document to "
|
|
"know the complete migration steps: "
|
|
"https://docs.openstack.org/nova/latest/configuration"
|
|
"/policy-concepts.html. If you did not intend to migrate "
|
|
"to new defaults in this upgrade, then with your current "
|
|
"policy file the scope checking rule will fail. A possible "
|
|
"reason for such a policy file is that you generated it with "
|
|
"'oslopolicy-sample-generator' in json format. "
|
|
"Three ways to fix this until you are ready to migrate to "
|
|
"scoped policies: 1. Generate the policy file with "
|
|
"'oslopolicy-sample-generator' in yaml format, keep "
|
|
"the generated content commented out, and update "
|
|
"the generated policy.yaml location in "
|
|
"``oslo_policy.policy_file``. "
|
|
"2. Use a pre-existing sample config file from the Train "
|
|
"release. 3. Use an empty or non-existent file to take all "
|
|
"the defaults.")
|
|
rule = "system_admin_api"
|
|
rule_new_default = "role:admin and system_scope:all"
|
|
status = upgradecheck.Result(upgradecheck.Code.SUCCESS)
|
|
# NOTE(gmann): Initialise the policy if it not initialized.
|
|
# We need policy enforcer with all the rules loaded to check
|
|
# their value with defaults.
|
|
try:
|
|
if policy._ENFORCER is None:
|
|
policy.init(suppress_deprecation_warnings=True)
|
|
|
|
# For safer side, recheck that the enforcer is available before
|
|
# upgrade checks. If something is wrong on oslo side and enforcer
|
|
# is still not available the return warning to avoid any false
|
|
# result.
|
|
if policy._ENFORCER is not None:
|
|
current_rule = str(policy._ENFORCER.rules[rule]).strip("()")
|
|
if (current_rule == rule_new_default and
|
|
not CONF.oslo_policy.enforce_scope):
|
|
status = upgradecheck.Result(upgradecheck.Code.WARNING,
|
|
msg)
|
|
else:
|
|
status = upgradecheck.Result(
|
|
upgradecheck.Code.WARNING,
|
|
_('Policy is not initialized to check the policy rules'))
|
|
except Exception as ex:
|
|
status = upgradecheck.Result(
|
|
upgradecheck.Code.WARNING,
|
|
_('Unable to perform policy checks due to error: %s') %
|
|
str(ex))
|
|
# reset the policy state so that it can be initialized from fresh if
|
|
# operator changes policy file after running this upgrade checks.
|
|
policy.reset()
|
|
return status
|
|
|
|
def _check_old_computes(self):
|
|
# warn if there are computes in the system older than the previous
|
|
# major release
|
|
try:
|
|
utils.raise_if_old_compute()
|
|
except exception.TooOldComputeService as e:
|
|
return upgradecheck.Result(upgradecheck.Code.WARNING, str(e))
|
|
|
|
return upgradecheck.Result(upgradecheck.Code.SUCCESS)
|
|
|
|
def _check_machine_type_set(self):
|
|
ctxt = nova_context.get_admin_context()
|
|
if machine_type_utils.get_instances_without_type(ctxt):
|
|
msg = (_("""
|
|
Instances found without hw_machine_type set. This warning can be ignored if
|
|
your environment does not contain libvirt based compute hosts.
|
|
Use the `nova-manage machine_type list_unset` command to list these instances.
|
|
For more details see the following:
|
|
https://docs.openstack.org/latest/nova/admin/hw_machine_type.html"""))
|
|
return upgradecheck.Result(upgradecheck.Code.WARNING, msg)
|
|
|
|
return upgradecheck.Result(upgradecheck.Code.SUCCESS)
|
|
|
|
# The format of the check functions is to return an upgradecheck.Result
|
|
# object with the appropriate upgradecheck.Code and details set. If the
|
|
# check hits warnings or failures then those should be stored in the
|
|
# returned upgradecheck.Result's "details" attribute. The summary will
|
|
# be rolled up at the end of the check() function. These functions are
|
|
# intended to be run in order and build on top of each other so order
|
|
# matters.
|
|
_upgrade_checks = (
|
|
# Added in Ocata
|
|
(_('Cells v2'), _check_cellsv2),
|
|
# Added in Ocata
|
|
(_('Placement API'), _check_placement),
|
|
# Added in Train
|
|
(_('Cinder API'), _check_cinder),
|
|
# Added in Ussuri
|
|
(_('Policy Scope-based Defaults'), _check_policy),
|
|
# Added in Victoria
|
|
(
|
|
_('Policy File JSON to YAML Migration'),
|
|
(common_checks.check_policy_json, {'conf': CONF})
|
|
),
|
|
# Added in Wallaby
|
|
(_('Older than N-1 computes'), _check_old_computes),
|
|
# Added in Wallaby
|
|
(_('hw_machine_type unset'), _check_machine_type_set),
|
|
)
|
|
|
|
|
|
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 255 so it's not confused with the upgrade check exit codes.
|
|
return 255
|