Coverage for nova/cmd/status.py: 100%
138 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
1# Copyright 2016 IBM Corp.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
15"""
16CLI interface for nova status commands.
17"""
19import functools
20import sys
21import traceback
23from keystoneauth1 import exceptions as ks_exc
24import microversion_parse
25from oslo_config import cfg
26from oslo_upgradecheck import common_checks
27from oslo_upgradecheck import upgradecheck
28import sqlalchemy as sa
29from sqlalchemy import func as sqlfunc
31from nova.cmd import common as cmd_common
32import nova.conf
33from nova import config
34from nova import context as nova_context
35from nova.db.api import api as api_db_api
36from nova.db.main import api as main_db_api
37from nova import exception
38from nova.i18n import _
39from nova.objects import cell_mapping as cell_mapping_obj
40# NOTE(lyarwood): The following are imported as machine_type_utils expects them
41# to be registered under nova.objects when called via _check_machine_type_set
42from nova.objects import image_meta as image_meta_obj # noqa: F401
43from nova.objects import instance as instance_obj # noqa: F401
44from nova import utils
45from nova import version
46from nova.virt.libvirt import machine_type_utils
47from nova.volume import cinder
49CONF = nova.conf.CONF
51# NOTE(gibi): 1.36 is required by nova-scheduler to support the same_subtree
52# queryparam to make GET /allocation_candidates require that a list of request
53# groups are satisfied from the same provider subtree.
54# NOTE: If you bump this version, remember to update the history
55# section in the nova-status man page (doc/source/cli/nova-status).
56MIN_PLACEMENT_MICROVERSION = "1.36"
58# NOTE(mriedem): 3.44 is needed to work with volume attachment records which
59# are required for supporting multi-attach capable volumes.
60MIN_CINDER_MICROVERSION = '3.44'
63class UpgradeCommands(upgradecheck.UpgradeCommands):
64 """Commands related to upgrades.
66 The subcommands here must not rely on the nova object model since they
67 should be able to run on n-1 data. Any queries to the database should be
68 done through the sqlalchemy query language directly like the database
69 schema migrations.
70 """
72 def _count_compute_nodes(self, context=None):
73 """Returns the number of compute nodes in the cell database."""
74 # NOTE(mriedem): This does not filter based on the service status
75 # because a disabled nova-compute service could still be reporting
76 # inventory info to the placement service. There could be an outside
77 # chance that there are compute node records in the database for
78 # disabled nova-compute services that aren't yet upgraded to Ocata or
79 # the nova-compute service was deleted and the service isn't actually
80 # running on the compute host but the operator hasn't cleaned up the
81 # compute_nodes entry in the database yet. We consider those edge cases
82 # here and the worst case scenario is we give a warning that there are
83 # more compute nodes than resource providers. We can tighten this up
84 # later if needed, for example by not including compute nodes that
85 # don't have a corresponding nova-compute service in the services
86 # table, or by only counting compute nodes with a service version of at
87 # least 15 which was the highest service version when Newton was
88 # released.
89 meta = sa.MetaData()
90 engine = main_db_api.get_engine(context=context)
91 compute_nodes = sa.Table('compute_nodes', meta, autoload_with=engine)
92 with engine.connect() as conn:
93 return conn.execute(
94 sa.select(sqlfunc.count()).select_from(compute_nodes).where(
95 compute_nodes.c.deleted == 0
96 )
97 ).scalars().first()
99 def _check_cellsv2(self):
100 """Checks to see if cells v2 has been setup.
102 These are the same checks performed in the 030_require_cell_setup API
103 DB migration except we expect this to be run AFTER the
104 nova-manage cell_v2 simple_cell_setup command, which would create the
105 cell and host mappings and sync the cell0 database schema, so we don't
106 check for flavors at all because you could create those after doing
107 this on an initial install. This also has to be careful about checking
108 for compute nodes if there are no host mappings on a fresh install.
109 """
110 meta = sa.MetaData()
111 engine = api_db_api.get_engine()
113 cell_mappings = self._get_cell_mappings()
114 count = len(cell_mappings)
115 # Two mappings are required at a minimum, cell0 and your first cell
116 if count < 2:
117 msg = _('There needs to be at least two cell mappings, one for '
118 'cell0 and one for your first cell. Run command '
119 '\'nova-manage cell_v2 simple_cell_setup\' and then '
120 'retry.')
121 return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
123 cell0 = any(mapping.is_cell0() for mapping in cell_mappings)
124 if not cell0:
125 msg = _('No cell0 mapping found. Run command '
126 '\'nova-manage cell_v2 simple_cell_setup\' and then '
127 'retry.')
128 return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
130 host_mappings = sa.Table('host_mappings', meta, autoload_with=engine)
132 with engine.connect() as conn:
133 count = conn.execute(
134 sa.select(sqlfunc.count()).select_from(host_mappings)
135 ).scalars().first()
137 if count == 0:
138 # This may be a fresh install in which case there may not be any
139 # compute_nodes in the cell database if the nova-compute service
140 # hasn't started yet to create those records. So let's query the
141 # cell database for compute_nodes records and if we find at least
142 # one it's a failure.
143 num_computes = self._count_compute_nodes()
144 if num_computes > 0:
145 msg = _('No host mappings found but there are compute nodes. '
146 'Run command \'nova-manage cell_v2 '
147 'simple_cell_setup\' and then retry.')
148 return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
150 msg = _('No host mappings or compute nodes were found. Remember '
151 'to run command \'nova-manage cell_v2 discover_hosts\' '
152 'when new compute hosts are deployed.')
153 return upgradecheck.Result(upgradecheck.Code.SUCCESS, msg)
155 return upgradecheck.Result(upgradecheck.Code.SUCCESS)
157 @staticmethod
158 def _placement_get(path):
159 """Do an HTTP get call against placement engine.
161 This is in a dedicated method to make it easier for unit
162 testing purposes.
164 """
165 client = utils.get_ksa_adapter('placement')
166 return client.get(path, raise_exc=True).json()
168 def _check_placement(self):
169 """Checks to see if the placement API is ready for scheduling.
171 Checks to see that the placement API service is registered in the
172 service catalog and that we can make requests against it.
173 """
174 try:
175 # TODO(efried): Use ksa's version filtering in _placement_get
176 versions = self._placement_get("/")
177 max_version = microversion_parse.parse_version_string(
178 versions["versions"][0]["max_version"])
179 needs_version = microversion_parse.parse_version_string(
180 MIN_PLACEMENT_MICROVERSION)
181 if max_version < needs_version:
182 msg = (_('Placement API version %(needed)s needed, '
183 'you have %(current)s.') %
184 {'needed': needs_version, 'current': max_version})
185 return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
186 except ks_exc.MissingAuthPlugin:
187 msg = _('No credentials specified for placement API in nova.conf.')
188 return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
189 except ks_exc.Unauthorized:
190 msg = _('Placement service credentials do not work.')
191 return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
192 except ks_exc.EndpointNotFound:
193 msg = _('Placement API endpoint not found.')
194 return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
195 except ks_exc.DiscoveryFailure:
196 msg = _('Discovery for placement API URI failed.')
197 return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
198 except ks_exc.NotFound:
199 msg = _('Placement API does not seem to be running.')
200 return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
202 return upgradecheck.Result(upgradecheck.Code.SUCCESS)
204 @staticmethod
205 def _get_cell_mappings():
206 """Queries the API database for cell mappings.
208 .. note:: This method is unique in that it queries the database using
209 CellMappingList.get_all() rather than a direct query using
210 the sqlalchemy models. This is because
211 CellMapping.database_connection can be a template and the
212 object takes care of formatting the URL. We cannot use
213 RowProxy objects from sqlalchemy because we cannot set the
214 formatted 'database_connection' value back on those objects
215 (they are read-only).
217 :returns: list of nova.objects.CellMapping objects
218 """
219 ctxt = nova_context.get_admin_context()
220 cell_mappings = cell_mapping_obj.CellMappingList.get_all(ctxt)
221 return cell_mappings
223 def _check_cinder(self):
224 """Checks to see that the cinder API is available at a given minimum
225 microversion.
226 """
227 # Check to see if nova is even configured for Cinder yet (fresh install
228 # or maybe not using Cinder at all).
229 if CONF.cinder.auth_type is None:
230 return upgradecheck.Result(upgradecheck.Code.SUCCESS)
232 try:
233 # TODO(mriedem): Eventually use get_ksa_adapter here when it
234 # supports cinder.
235 cinder.is_microversion_supported(
236 nova_context.get_admin_context(), MIN_CINDER_MICROVERSION)
237 except exception.CinderAPIVersionNotAvailable:
238 return upgradecheck.Result(
239 upgradecheck.Code.FAILURE,
240 _('Cinder API %s or greater is required. Deploy at least '
241 'Cinder 12.0.0 (Queens).') % MIN_CINDER_MICROVERSION)
242 except Exception as ex:
243 # Anything else trying to connect, like bad config, is out of our
244 # hands so just return a warning.
245 return upgradecheck.Result(
246 upgradecheck.Code.WARNING,
247 _('Unable to determine Cinder API version due to error: %s') %
248 str(ex))
249 return upgradecheck.Result(upgradecheck.Code.SUCCESS)
251 def _check_old_computes(self):
252 # warn if there are computes in the system older than the previous
253 # major release
254 try:
255 utils.raise_if_old_compute()
256 except exception.TooOldComputeService as e:
257 return upgradecheck.Result(upgradecheck.Code.FAILURE, str(e))
259 return upgradecheck.Result(upgradecheck.Code.SUCCESS)
261 def _check_machine_type_set(self):
262 ctxt = nova_context.get_admin_context()
263 if machine_type_utils.get_instances_without_type(ctxt):
264 msg = (_("""
265Instances found without hw_machine_type set. This warning can be ignored if
266your environment does not contain libvirt based compute hosts.
267Use the `nova-manage libvirt list_unset_machine_type` command to list these
268instances. For more details see the following:
269https://docs.openstack.org/nova/latest/admin/hw-machine-type.html"""))
270 return upgradecheck.Result(upgradecheck.Code.WARNING, msg)
272 return upgradecheck.Result(upgradecheck.Code.SUCCESS)
274 def _check_service_user_token(self):
275 if not CONF.service_user.send_service_user_token:
276 msg = (_("""
277Service user token configuration is required for all Nova services.
278For more details see the following:
279https://docs.openstack.org/nova/latest/admin/configuration/service-user-token.html""")) # noqa
280 return upgradecheck.Result(upgradecheck.Code.FAILURE, msg)
281 return upgradecheck.Result(upgradecheck.Code.SUCCESS)
283 # The format of the check functions is to return an upgradecheck.Result
284 # object with the appropriate upgradecheck.Code and details set. If the
285 # check hits warnings or failures then those should be stored in the
286 # returned upgradecheck.Result's "details" attribute. The summary will
287 # be rolled up at the end of the check() function. These functions are
288 # intended to be run in order and build on top of each other so order
289 # matters.
290 _upgrade_checks = (
291 # Added in Ocata
292 (_('Cells v2'), _check_cellsv2),
293 # Added in Ocata
294 (_('Placement API'), _check_placement),
295 # Added in Train
296 (_('Cinder API'), _check_cinder),
297 # Added in Victoria
298 (
299 _('Policy File JSON to YAML Migration'),
300 (common_checks.check_policy_json, {'conf': CONF})
301 ),
302 # Added in Wallaby
303 (_('Older than N-1 computes'), _check_old_computes),
304 # Added in Wallaby
305 (_('hw_machine_type unset'), _check_machine_type_set),
306 # Added in Bobcat
307 (_('Service User Token Configuration'), _check_service_user_token),
308 )
311CATEGORIES = {
312 'upgrade': UpgradeCommands,
313}
316add_command_parsers = functools.partial(cmd_common.add_command_parsers,
317 categories=CATEGORIES)
320category_opt = cfg.SubCommandOpt('category',
321 title='Command categories',
322 help='Available categories',
323 handler=add_command_parsers)
326def main():
327 """Parse options and call the appropriate class/method."""
328 CONF.register_cli_opt(category_opt)
329 config.parse_args(sys.argv)
331 if CONF.category.name == "version":
332 print(version.version_string_with_package())
333 return 0
335 if CONF.category.name == "bash-completion":
336 cmd_common.print_bash_completion(CATEGORIES)
337 return 0
339 try:
340 fn, fn_args, fn_kwargs = cmd_common.get_action_fn()
341 ret = fn(*fn_args, **fn_kwargs)
342 return ret
343 except Exception:
344 print(_('Error:\n%s') % traceback.format_exc())
345 # This is 255 so it's not confused with the upgrade check exit codes.
346 return 255