373 lines
14 KiB
Python
373 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
|
|
from urllib import parse as urlparse
|
|
|
|
import iso8601
|
|
from oslo_utils import timeutils
|
|
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=str(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): # noqa
|
|
"""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): # noqa
|
|
"""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
|