Billing statistics improved

Instance tracking information was improved to contain additional
information on instances such as type name, descriptive title
and number of VMs used by the application.

Added ability to retrieve non-aggregated statistics.
Also instance statistics API refactoring was made.

Implements blueprint app-catalog-billing

Change-Id: I8f3ea1a3947ceaf7deb2ee62b4ad41cbf230596e
This commit is contained in:
Stan Lagun
2014-04-10 21:56:15 +04:00
parent c76dff77fe
commit 4dd5c7b2f2
9 changed files with 153 additions and 35 deletions

View File

@@ -30,7 +30,8 @@ Workflow:
Body:
- $.primaryController.deploy()
- $.secondaryControllers.pselect($.deploy())
- $.reportDeployed()
- $.reportDeployed(title => 'MS Active Directory',
unitCount => len(secondaryControllers) + 1)
destroy:
- $.reportDestroyed()

View File

@@ -5,8 +5,15 @@ Name: Application
Workflow:
reportDeployed:
Arguments:
- title:
Contract: $.string()
Default: null
- unitCount:
Contract: $.int()
Default: null
Body:
- $this.find(Environment).instanceNotifier.trackApplication($this)
- $this.find(Environment).instanceNotifier.trackApplication($this, $title, $unitCount)
reportDestroyed:
Body:

View File

@@ -25,14 +25,14 @@ API_NAME = 'EnvironmentStatistics'
class Controller(object):
@request_statistics.stats_count(API_NAME, 'GetForEnvironment')
def get_for_environment(self, request, environment_id):
LOG.debug(_('EnvironmentStatistics:GetForEnvironment'))
@request_statistics.stats_count(API_NAME, 'GetAggregated')
def get_aggregated(self, request, environment_id):
LOG.debug(_('EnvironmentStatistics:GetAggregated'))
# TODO (stanlagun): Check that caller is authorized to access
# tenant's statistics
return instances.InstanceStatsServices.get_environment_stats(
return instances.InstanceStatsServices.get_aggregated_stats(
environment_id)
@request_statistics.stats_count(API_NAME, 'GetForInstance')
@@ -42,9 +42,19 @@ class Controller(object):
# TODO (stanlagun): Check that caller is authorized to access
# tenant's statistics
return instances.InstanceStatsServices.get_environment_stats(
return instances.InstanceStatsServices.get_raw_environment_stats(
environment_id, instance_id)
@request_statistics.stats_count(API_NAME, 'GetForEnvironment')
def get_for_environment(self, request, environment_id):
LOG.debug(_('EnvironmentStatistics:GetForEnvironment'))
# TODO (stanlagun): Check that caller is authorized to access
# tenant's statistics
return instances.InstanceStatsServices.get_raw_environment_stats(
environment_id)
def create_resource():
return wsgi.Resource(Controller())

View File

@@ -15,8 +15,8 @@ import routes
from muranoapi.api.v1 import catalog
from muranoapi.api.v1 import deployments
from muranoapi.api.v1 import environment_statistics
from muranoapi.api.v1 import environments
from muranoapi.api.v1 import instance_statistics
from muranoapi.api.v1 import request_statistics
from muranoapi.api.v1 import services
from muranoapi.api.v1 import sessions
@@ -122,17 +122,23 @@ class API(wsgi.Router):
action='deploy',
conditions={'method': ['POST']})
statistics_resource = environment_statistics.create_resource()
statistics_resource = instance_statistics.create_resource()
mapper.connect(
'/environments/{environment_id}/statistics/{instance_id}',
'/environments/{environment_id}/instance-statistics/raw/'
'{instance_id}',
controller=statistics_resource,
action='get_for_instance',
conditions={'method': ['GET']})
mapper.connect(
'/environments/{environment_id}/statistics',
'/environments/{environment_id}/instance-statistics/raw',
controller=statistics_resource,
action='get_for_environment',
conditions={'method': ['GET']})
mapper.connect(
'/environments/{environment_id}/instance-statistics/aggregated',
controller=statistics_resource,
action='get_aggregated',
conditions={'method': ['GET']})
catalog_resource = catalog.create_resource()
mapper.connect('/catalog/packages/categories',

View File

@@ -117,8 +117,13 @@ def track_instance(payload):
instance_id = payload['instance']
instance_type = payload.get('instance_type', 0)
environment_id = payload['environment']
unit_count = payload.get('unit_count')
type_name = payload['type_name']
type_title = payload.get('type_title')
instances.InstanceStatsServices.track_instance(
instance_id, environment_id, instance_type)
instance_id, environment_id, instance_type,
type_name, type_title, unit_count)
@notification_endpoint_wrapper()

View File

@@ -0,0 +1,43 @@
# Copyright (c) 2013 Mirantis, Inc.
#
# 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.
from sqlalchemy import schema
from sqlalchemy import types
meta = schema.MetaData()
def upgrade(migrate_engine):
meta.bind = migrate_engine
table = schema.Table('instance', meta, autoload=True)
table.rename('instance_stats')
table.delete()
type_name = schema.Column('type_name', types.String(512), nullable=False)
type_name.create(table)
type_title = schema.Column('type_title', types.String(512))
type_title.create(table)
unit_count = schema.Column('unit_count', types.Integer())
unit_count.create(table)
tenant_id = schema.Column('tenant_id', types.String(32), nullable=False)
tenant_id.create(table)
def downgrade(migrate_engine):
meta.bind = migrate_engine
table = schema.Table('instance_stats', meta, autoload=True)
table.rename('instance')
table.c.type_name.drop()
table.c.type_title.drop()
table.c.unit_count.drop()
table.c.tenant_id.drop()

View File

@@ -233,7 +233,7 @@ package_to_tag = sa.Table('package_to_tag',
class Instance(BASE, ModelBase):
__tablename__ = 'instance'
__tablename__ = 'instance_stats'
environment_id = sa.Column(
sa.String(100), primary_key=True, nullable=False)
@@ -242,6 +242,10 @@ class Instance(BASE, ModelBase):
instance_type = sa.Column(sa.Integer, default=0, nullable=False)
created = sa.Column(sa.Integer, nullable=False)
destroyed = sa.Column(sa.Integer, nullable=True)
type_name = sa.Column('type_name', sa.String(512), nullable=False)
type_title = sa.Column('type_title', sa.String(512))
unit_count = sa.Column('unit_count', sa.Integer())
tenant_id = sa.Column('tenant_id', sa.String(32), nullable=False)
def to_dict(self):
dictionary = super(Instance, self).to_dict()

View File

@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import sqlalchemy
from sqlalchemy.sql import func
from muranoapi.db import models
@@ -27,20 +29,31 @@ OS_INSTANCE = 200
class InstanceStatsServices(object):
@staticmethod
def track_instance(instance_id, environment_id, instance_type):
instance = models.Instance()
instance.instance_id = instance_id
instance.environment_id = environment_id
instance.instance_type = instance_type
instance.created = timeutils.utcnow_ts()
instance.destroyed = None
def track_instance(instance_id, environment_id, instance_type,
type_name, type_title=None, unit_count=None):
unit = db_session.get_session()
try:
with unit.begin():
env = unit.query(models.Environment).get(environment_id)
instance = models.Instance()
instance.instance_id = instance_id
instance.environment_id = environment_id
instance.tenant_id = env.tenant_id
instance.instance_type = instance_type
instance.created = timeutils.utcnow_ts()
instance.destroyed = None
instance.type_name = type_name
instance.type_title = type_title
instance.unit_count = unit_count
unit.add(instance)
except exception.DBDuplicateEntry:
pass # expected behaviour when record already exists
unit.execute(
sqlalchemy.update(models.Instance).where(
models.Instance.instance_id == instance_id and
models.Instance.environment_id == environment_id).values(
unit_count=unit_count))
@staticmethod
def destroy_instance(instance_id, environment_id):
@@ -52,16 +65,13 @@ class InstanceStatsServices(object):
instance.save(unit)
@staticmethod
def get_environment_stats(environment_id, instance_id=None):
def get_aggregated_stats(environment_id):
unit = db_session.get_session()
now = timeutils.utcnow_ts()
query = unit.query(models.Instance.instance_type, func.sum(
func.coalesce(models.Instance.destroyed, now) -
models.Instance.created), func.count()).filter(
models.Instance.environment_id == environment_id)
if instance_id is not None:
query = query.filter(
models.Instance.instance_id == instance_id)
res = query.group_by(models.Instance.instance_type).all()
@@ -70,3 +80,25 @@ class InstanceStatsServices(object):
'duration': int(record[1]),
'count': int(record[2])
} for record in res]
@staticmethod
def get_raw_environment_stats(environment_id, instance_id=None):
unit = db_session.get_session()
now = timeutils.utcnow_ts()
query = unit.query(models.Instance).filter(
models.Instance.environment_id == environment_id)
if instance_id:
query = query.filter(models.Instance.instance_id == instance_id)
res = query.all()
return [{
'type': record.instance_type,
'duration': (record.destroyed or now) - record.created,
'type_name': record.type_name,
'unit_count': record.unit_count,
'instance_id': record.instance_id,
'type_title': record.type_title,
'active': True if not record.destroyed else False
} for record in res]

View File

@@ -44,26 +44,36 @@ class InstanceReportNotifier(object):
def initialize(self, environment):
self._environment_id = environment.object_id
def _track_instance(self, instance, instance_type, untrack=False):
def _track_instance(self, instance, instance_type,
type_title, unit_count):
payload = {
'instance': instance.object_id,
'environment': self._environment_id,
'instance_type': instance_type
'instance_type': instance_type,
'type_name': instance.type.name,
'type_title': type_title,
'unit_count': unit_count
}
event_type = 'murano.untrack_instance' \
if untrack else 'murano.track_instance'
self._notifier.info({}, 'murano.track_instance', payload)
self._notifier.info({}, event_type, payload)
def _untrack_instance(self, instance, instance_type):
payload = {
'instance': instance.object_id,
'environment': self._environment_id,
'instance_type': instance_type,
}
def trackApplication(self, instance):
self._track_instance(instance, APPLICATION, False)
self._notifier.info({}, 'murano.untrack_instance', payload)
def trackApplication(self, instance, title=None, unitCount=None):
self._track_instance(instance, APPLICATION, title, unitCount)
def untrackApplication(self, instance):
self._track_instance(instance, APPLICATION, True)
self._untrack_instance(instance, APPLICATION)
def trackCloudInstance(self, instance):
self._track_instance(instance, OS_INSTANCE, False)
self._track_instance(instance, OS_INSTANCE, None, 1)
def untrackCloudInstance(self, instance):
self._track_instance(instance, OS_INSTANCE, True)
self._untrack_instance(instance, OS_INSTANCE)