b26bc7fd7a
This microversion implements below API cleanups: 1. 400 for unknown param for query param and for request body. 2. Making server representation always consistent among all APIs returning the complete server representation. 3. Change the default return value of ``swap`` field from the empty string to 0 (integer) in flavor APIs. 4. Return ``servers`` field always in the response of GET hypervisors API even there are no servers on hypervisor Details: https://specs.openstack.org/openstack/nova-specs/specs/train/approved/api-consistency-cleanup.html Partial-Implements: blueprint api-consistency-cleanup Change-Id: I9d257a003d315b84b937dcef91f3cb41f3e24b53
374 lines
14 KiB
Python
374 lines
14 KiB
Python
# Copyright 2011 OpenStack Foundation
|
|
# 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 collections
|
|
import datetime
|
|
|
|
import iso8601
|
|
from oslo_utils import timeutils
|
|
import six
|
|
import six.moves.urllib.parse as urlparse
|
|
from webob import exc
|
|
|
|
from nova.api.openstack import common
|
|
from nova.api.openstack.compute.schemas import simple_tenant_usage as schema
|
|
from nova.api.openstack.compute.views import usages as usages_view
|
|
from nova.api.openstack import wsgi
|
|
from nova.api import validation
|
|
import nova.conf
|
|
from nova import context as nova_context
|
|
from nova import exception
|
|
from nova.i18n import _
|
|
from nova import objects
|
|
from nova.policies import simple_tenant_usage as stu_policies
|
|
|
|
CONF = nova.conf.CONF
|
|
|
|
|
|
def parse_strtime(dstr, fmt):
|
|
try:
|
|
return timeutils.parse_strtime(dstr, fmt)
|
|
except (TypeError, ValueError) as e:
|
|
raise exception.InvalidStrTime(reason=six.text_type(e))
|
|
|
|
|
|
class SimpleTenantUsageController(wsgi.Controller):
|
|
|
|
_view_builder_class = usages_view.ViewBuilder
|
|
|
|
def _hours_for(self, instance, period_start, period_stop):
|
|
launched_at = instance.launched_at
|
|
terminated_at = instance.terminated_at
|
|
if terminated_at is not None:
|
|
if not isinstance(terminated_at, datetime.datetime):
|
|
# NOTE(mriedem): Instance object DateTime fields are
|
|
# timezone-aware so convert using isotime.
|
|
terminated_at = timeutils.parse_isotime(terminated_at)
|
|
|
|
if launched_at is not None:
|
|
if not isinstance(launched_at, datetime.datetime):
|
|
launched_at = timeutils.parse_isotime(launched_at)
|
|
|
|
if terminated_at and terminated_at < period_start:
|
|
return 0
|
|
# nothing if it started after the usage report ended
|
|
if launched_at and launched_at > period_stop:
|
|
return 0
|
|
if launched_at:
|
|
# if instance launched after period_started, don't charge for first
|
|
start = max(launched_at, period_start)
|
|
if terminated_at:
|
|
# if instance stopped before period_stop, don't charge after
|
|
stop = min(period_stop, terminated_at)
|
|
else:
|
|
# instance is still running, so charge them up to current time
|
|
stop = period_stop
|
|
dt = stop - start
|
|
return dt.total_seconds() / 3600.0
|
|
else:
|
|
# instance hasn't launched, so no charge
|
|
return 0
|
|
|
|
def _get_flavor(self, context, instance, flavors_cache):
|
|
"""Get flavor information from the instance object,
|
|
allowing a fallback to lookup by-id for deleted instances only.
|
|
"""
|
|
try:
|
|
return instance.get_flavor()
|
|
except exception.NotFound:
|
|
if not instance.deleted:
|
|
# Only support the fallback mechanism for deleted instances
|
|
# that would have been skipped by migration #153
|
|
raise
|
|
|
|
flavor_type = instance.instance_type_id
|
|
if flavor_type in flavors_cache:
|
|
return flavors_cache[flavor_type]
|
|
|
|
try:
|
|
flavor_ref = objects.Flavor.get_by_id(context, flavor_type)
|
|
flavors_cache[flavor_type] = flavor_ref
|
|
except exception.FlavorNotFound:
|
|
# can't bill if there is no flavor
|
|
flavor_ref = None
|
|
|
|
return flavor_ref
|
|
|
|
def _get_instances_all_cells(self, context, period_start, period_stop,
|
|
tenant_id, limit, marker):
|
|
all_instances = []
|
|
cells = objects.CellMappingList.get_all(context)
|
|
for cell in cells:
|
|
with nova_context.target_cell(context, cell) as cctxt:
|
|
try:
|
|
instances = (
|
|
objects.InstanceList.get_active_by_window_joined(
|
|
cctxt, period_start, period_stop, tenant_id,
|
|
expected_attrs=['flavor'], limit=limit,
|
|
marker=marker))
|
|
except exception.MarkerNotFound:
|
|
# NOTE(danms): We need to keep looking through the later
|
|
# cells to find the marker
|
|
continue
|
|
all_instances.extend(instances)
|
|
# NOTE(danms): We must have found a marker if we had one,
|
|
# so make sure we don't require a marker in the next cell
|
|
marker = None
|
|
if limit:
|
|
limit -= len(instances)
|
|
if limit <= 0:
|
|
break
|
|
if marker is not None and len(all_instances) == 0:
|
|
# NOTE(danms): If we did not find the marker in any cell,
|
|
# mimic the db_api behavior here
|
|
raise exception.MarkerNotFound(marker=marker)
|
|
|
|
return all_instances
|
|
|
|
def _tenant_usages_for_period(self, context, period_start, period_stop,
|
|
tenant_id=None, detailed=True, limit=None,
|
|
marker=None):
|
|
instances = self._get_instances_all_cells(context, period_start,
|
|
period_stop, tenant_id,
|
|
limit, marker)
|
|
rval = collections.OrderedDict()
|
|
flavors = {}
|
|
all_server_usages = []
|
|
|
|
for instance in instances:
|
|
info = {}
|
|
info['hours'] = self._hours_for(instance,
|
|
period_start,
|
|
period_stop)
|
|
flavor = self._get_flavor(context, instance, flavors)
|
|
if not flavor:
|
|
info['flavor'] = ''
|
|
else:
|
|
info['flavor'] = flavor.name
|
|
|
|
info['instance_id'] = instance.uuid
|
|
info['name'] = instance.display_name
|
|
info['tenant_id'] = instance.project_id
|
|
|
|
try:
|
|
info['memory_mb'] = instance.flavor.memory_mb
|
|
info['local_gb'] = (instance.flavor.root_gb +
|
|
instance.flavor.ephemeral_gb)
|
|
info['vcpus'] = instance.flavor.vcpus
|
|
except exception.InstanceNotFound:
|
|
# This is rare case, instance disappear during analysis
|
|
# As it's just info collection, we can try next one
|
|
continue
|
|
|
|
# NOTE(mriedem): We need to normalize the start/end times back
|
|
# to timezone-naive so the response doesn't change after the
|
|
# conversion to objects.
|
|
info['started_at'] = timeutils.normalize_time(instance.launched_at)
|
|
|
|
info['ended_at'] = (
|
|
timeutils.normalize_time(instance.terminated_at) if
|
|
instance.terminated_at else None)
|
|
|
|
if info['ended_at']:
|
|
info['state'] = 'terminated'
|
|
else:
|
|
info['state'] = instance.vm_state
|
|
|
|
now = timeutils.utcnow()
|
|
|
|
if info['state'] == 'terminated':
|
|
delta = info['ended_at'] - info['started_at']
|
|
else:
|
|
delta = now - info['started_at']
|
|
|
|
info['uptime'] = int(delta.total_seconds())
|
|
|
|
if info['tenant_id'] not in rval:
|
|
summary = {}
|
|
summary['tenant_id'] = info['tenant_id']
|
|
if detailed:
|
|
summary['server_usages'] = []
|
|
summary['total_local_gb_usage'] = 0
|
|
summary['total_vcpus_usage'] = 0
|
|
summary['total_memory_mb_usage'] = 0
|
|
summary['total_hours'] = 0
|
|
summary['start'] = timeutils.normalize_time(period_start)
|
|
summary['stop'] = timeutils.normalize_time(period_stop)
|
|
rval[info['tenant_id']] = summary
|
|
|
|
summary = rval[info['tenant_id']]
|
|
summary['total_local_gb_usage'] += info['local_gb'] * info['hours']
|
|
summary['total_vcpus_usage'] += info['vcpus'] * info['hours']
|
|
summary['total_memory_mb_usage'] += (info['memory_mb'] *
|
|
info['hours'])
|
|
|
|
summary['total_hours'] += info['hours']
|
|
all_server_usages.append(info)
|
|
if detailed:
|
|
summary['server_usages'].append(info)
|
|
|
|
return list(rval.values()), all_server_usages
|
|
|
|
def _parse_datetime(self, dtstr):
|
|
if not dtstr:
|
|
value = timeutils.utcnow()
|
|
elif isinstance(dtstr, datetime.datetime):
|
|
value = dtstr
|
|
else:
|
|
for fmt in ["%Y-%m-%dT%H:%M:%S",
|
|
"%Y-%m-%dT%H:%M:%S.%f",
|
|
"%Y-%m-%d %H:%M:%S.%f"]:
|
|
try:
|
|
value = parse_strtime(dtstr, fmt)
|
|
break
|
|
except exception.InvalidStrTime:
|
|
pass
|
|
else:
|
|
msg = _("Datetime is in invalid format")
|
|
raise exception.InvalidStrTime(reason=msg)
|
|
|
|
# NOTE(mriedem): Instance object DateTime fields are timezone-aware
|
|
# so we have to force UTC timezone for comparing this datetime against
|
|
# instance object fields and still maintain backwards compatibility
|
|
# in the API.
|
|
if value.utcoffset() is None:
|
|
value = value.replace(tzinfo=iso8601.UTC)
|
|
return value
|
|
|
|
def _get_datetime_range(self, req):
|
|
qs = req.environ.get('QUERY_STRING', '')
|
|
env = urlparse.parse_qs(qs)
|
|
# NOTE(lzyeval): env.get() always returns a list
|
|
period_start = self._parse_datetime(env.get('start', [None])[0])
|
|
period_stop = self._parse_datetime(env.get('end', [None])[0])
|
|
|
|
if not period_start < period_stop:
|
|
msg = _("Invalid start time. The start time cannot occur after "
|
|
"the end time.")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
detailed = env.get('detailed', ['0'])[0] == '1'
|
|
return (period_start, period_stop, detailed)
|
|
|
|
@wsgi.Controller.api_version("2.40")
|
|
@validation.query_schema(schema.index_query_275, '2.75')
|
|
@validation.query_schema(schema.index_query_v240, '2.40', '2.74')
|
|
@wsgi.expected_errors(400)
|
|
def index(self, req):
|
|
"""Retrieve tenant_usage for all tenants."""
|
|
return self._index(req, links=True)
|
|
|
|
@wsgi.Controller.api_version("2.1", "2.39") # noqa
|
|
@validation.query_schema(schema.index_query)
|
|
@wsgi.expected_errors(400)
|
|
def index(self, req):
|
|
"""Retrieve tenant_usage for all tenants."""
|
|
return self._index(req)
|
|
|
|
@wsgi.Controller.api_version("2.40")
|
|
@validation.query_schema(schema.show_query_275, '2.75')
|
|
@validation.query_schema(schema.show_query_v240, '2.40', '2.74')
|
|
@wsgi.expected_errors(400)
|
|
def show(self, req, id):
|
|
"""Retrieve tenant_usage for a specified tenant."""
|
|
return self._show(req, id, links=True)
|
|
|
|
@wsgi.Controller.api_version("2.1", "2.39") # noqa
|
|
@validation.query_schema(schema.show_query)
|
|
@wsgi.expected_errors(400)
|
|
def show(self, req, id):
|
|
"""Retrieve tenant_usage for a specified tenant."""
|
|
return self._show(req, id)
|
|
|
|
def _index(self, req, links=False):
|
|
context = req.environ['nova.context']
|
|
|
|
context.can(stu_policies.POLICY_ROOT % 'list')
|
|
|
|
try:
|
|
(period_start, period_stop, detailed) = self._get_datetime_range(
|
|
req)
|
|
except exception.InvalidStrTime as e:
|
|
raise exc.HTTPBadRequest(explanation=e.format_message())
|
|
|
|
now = timeutils.parse_isotime(timeutils.utcnow().isoformat())
|
|
if period_stop > now:
|
|
period_stop = now
|
|
|
|
marker = None
|
|
limit = CONF.api.max_limit
|
|
if links:
|
|
limit, marker = common.get_limit_and_marker(req)
|
|
|
|
try:
|
|
usages, server_usages = self._tenant_usages_for_period(
|
|
context, period_start, period_stop, detailed=detailed,
|
|
limit=limit, marker=marker)
|
|
except exception.MarkerNotFound as e:
|
|
raise exc.HTTPBadRequest(explanation=e.format_message())
|
|
|
|
tenant_usages = {'tenant_usages': usages}
|
|
|
|
if links:
|
|
usages_links = self._view_builder.get_links(req, server_usages)
|
|
if usages_links:
|
|
tenant_usages['tenant_usages_links'] = usages_links
|
|
|
|
return tenant_usages
|
|
|
|
def _show(self, req, id, links=False):
|
|
tenant_id = id
|
|
context = req.environ['nova.context']
|
|
|
|
context.can(stu_policies.POLICY_ROOT % 'show',
|
|
{'project_id': tenant_id})
|
|
|
|
try:
|
|
(period_start, period_stop, ignore) = self._get_datetime_range(
|
|
req)
|
|
except exception.InvalidStrTime as e:
|
|
raise exc.HTTPBadRequest(explanation=e.format_message())
|
|
|
|
now = timeutils.parse_isotime(timeutils.utcnow().isoformat())
|
|
if period_stop > now:
|
|
period_stop = now
|
|
|
|
marker = None
|
|
limit = CONF.api.max_limit
|
|
if links:
|
|
limit, marker = common.get_limit_and_marker(req)
|
|
|
|
try:
|
|
usage, server_usages = self._tenant_usages_for_period(
|
|
context, period_start, period_stop, tenant_id=tenant_id,
|
|
detailed=True, limit=limit, marker=marker)
|
|
except exception.MarkerNotFound as e:
|
|
raise exc.HTTPBadRequest(explanation=e.format_message())
|
|
|
|
if len(usage):
|
|
usage = list(usage)[0]
|
|
else:
|
|
usage = {}
|
|
|
|
tenant_usage = {'tenant_usage': usage}
|
|
|
|
if links:
|
|
usages_links = self._view_builder.get_links(
|
|
req, server_usages, tenant_id=tenant_id)
|
|
if usages_links:
|
|
tenant_usage['tenant_usage_links'] = usages_links
|
|
|
|
return tenant_usage
|