nova/nova/api/openstack/compute/plugins/v3/simple_tenant_usage.py

265 lines
10 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 datetime
import urlparse
from webob import exc
from nova.api.openstack import extensions
from nova.compute import api
from nova.compute import flavors
from nova import exception
from nova.openstack.common.gettextutils import _
from nova.openstack.common import timeutils
ALIAS = 'os-simple-tenant-usage'
authorize_show = extensions.extension_authorizer('compute',
'v3:' + ALIAS + ':show')
authorize_list = extensions.extension_authorizer('compute',
'v3:' + ALIAS + ':list')
VALID_DATETIME_FORMAT = ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f",
"%Y-%m-%d %H:%M:%S.%f"]
class SimpleTenantUsageController(object):
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):
terminated_at = timeutils.parse_strtime(terminated_at,
"%Y-%m-%d %H:%M:%S.%f")
if launched_at is not None:
if not isinstance(launched_at, datetime.datetime):
launched_at = timeutils.parse_strtime(launched_at,
"%Y-%m-%d %H:%M:%S.%f")
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
seconds = (dt.days * 3600 * 24 + dt.seconds +
dt.microseconds / 100000.0)
return seconds / 3600.0
else:
# instance hasn't launched, so no charge
return 0
def _get_flavor(self, context, compute_api, instance, flavors_cache):
"""Get flavor information from the instance's system_metadata,
allowing a fallback to lookup by-id for deleted instances only.
"""
try:
return flavors.extract_flavor(instance)
except KeyError:
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:
it_ref = compute_api.get_instance_type(context, flavor_type)
flavors_cache[flavor_type] = it_ref
except exception.FlavorNotFound:
# can't bill if there is no instance type
it_ref = None
return it_ref
def _tenant_usages_for_period(self, context, period_start,
period_stop, tenant_id=None, detailed=True):
compute_api = api.API()
instances = compute_api.get_active_by_window(context,
period_start,
period_stop,
tenant_id)
rval = {}
flavors = {}
for instance in instances:
info = {}
info['hours'] = self._hours_for(instance,
period_start,
period_stop)
flavor = self._get_flavor(context, compute_api, instance, flavors)
if not flavor:
continue
info['instance_id'] = instance['uuid']
info['name'] = instance['display_name']
info['memory_mb'] = flavor['memory_mb']
info['local_gb'] = flavor['root_gb'] + flavor['ephemeral_gb']
info['vcpus'] = flavor['vcpus']
info['tenant_id'] = instance['project_id']
info['flavor'] = flavor['name']
info['started_at'] = instance['launched_at']
info['ended_at'] = instance['terminated_at']
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'] = delta.days * 24 * 3600 + delta.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'] = period_start
summary['stop'] = 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']
if detailed:
summary['server_usages'].append(info)
return rval.values()
def _parse_datetime(self, dtstr):
if not dtstr:
return timeutils.utcnow()
elif isinstance(dtstr, datetime.datetime):
return dtstr
for format in VALID_DATETIME_FORMAT:
try:
return timeutils.parse_strtime(dtstr, format)
except ValueError:
continue
return None
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])
if not period_start:
msg = _("Start time is invalid format, valid "
"formats are %s") % VALID_DATETIME_FORMAT
raise exc.HTTPBadRequest(explanation=msg)
period_stop = self._parse_datetime(env.get('end', [None])[0])
if not period_stop:
msg = _("Stop time is invalid format, valid "
"formats are %s") % VALID_DATETIME_FORMAT
raise exc.HTTPBadRequest(explanation=msg)
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)
@extensions.expected_errors(400)
def index(self, req):
"""Retrieve tenant_usage for all tenants."""
context = req.environ['nova.context']
authorize_list(context)
(period_start, period_stop, detailed) = self._get_datetime_range(req)
now = timeutils.utcnow()
if period_stop > now:
period_stop = now
usages = self._tenant_usages_for_period(context,
period_start,
period_stop,
detailed=detailed)
return {'tenant_usages': usages}
@extensions.expected_errors(400)
def show(self, req, id):
"""Retrieve tenant_usage for a specified tenant."""
tenant_id = id
context = req.environ['nova.context']
authorize_show(context, {'project_id': tenant_id})
(period_start, period_stop, ignore) = self._get_datetime_range(req)
now = timeutils.utcnow()
if period_stop > now:
period_stop = now
usage = self._tenant_usages_for_period(context,
period_start,
period_stop,
tenant_id=tenant_id,
detailed=True)
if len(usage):
usage = usage[0]
else:
usage = {}
return {'tenant_usage': usage}
class SimpleTenantUsage(extensions.V3APIExtensionBase):
"""Simple tenant usage extension."""
name = "SimpleTenantUsage"
alias = ALIAS
version = 1
def get_resources(self):
res = [extensions.ResourceExtension('os-simple-tenant-usage',
SimpleTenantUsageController())]
return res
def get_controller_extensions(self):
"""It's an abstract function V3APIExtensionBase and the extension
will not be loaded without it.
"""
return []