diff --git a/ceilometer/storage/__init__.py b/ceilometer/storage/__init__.py index 927320066..72876d8fe 100644 --- a/ceilometer/storage/__init__.py +++ b/ceilometer/storage/__init__.py @@ -19,14 +19,13 @@ """ -import datetime import urlparse from oslo.config import cfg from stevedore import driver from ceilometer.openstack.common import log -from ceilometer.openstack.common import timeutils +from ceilometer import utils LOG = log.getLogger(__name__) @@ -85,17 +84,31 @@ class SampleFilter(object): resource=None, meter=None, source=None, metaquery={}): self.user = user self.project = project - self.start = self._sanitize_timestamp(start) - self.end = self._sanitize_timestamp(end) + self.start = utils.sanitize_timestamp(start) + self.end = utils.sanitize_timestamp(end) self.resource = resource self.meter = meter self.source = source self.metaquery = metaquery - def _sanitize_timestamp(self, timestamp): - """Return a naive utc datetime object.""" - if not timestamp: - return timestamp - if not isinstance(timestamp, datetime.datetime): - timestamp = timeutils.parse_isotime(timestamp) - return timeutils.normalize_time(timestamp) + +class EventFilter(object): + """Properties for building an Event query. + + :param start: UTC start datetime (mandatory) + :param end: UTC end datetime (mandatory) + :param event_name: the name of the event. None for all. + :param traits: the trait filter dict, all of which are optional + {'key': , + 't_string': , + 't_int': , + 't_datetime': + 't_float': } + currently, only one trait dict is supported. + """ + + def __init__(self, start, end, event_name=None, traits={}): + self.start = utils.sanitize_timestamp(start) + self.end = utils.sanitize_timestamp(end) + self.event_name = event_name + self.traits = traits diff --git a/ceilometer/storage/base.py b/ceilometer/storage/base.py index 5c8881ad3..4771c95e4 100644 --- a/ceilometer/storage/base.py +++ b/ceilometer/storage/base.py @@ -156,3 +156,15 @@ class Connection(object): @abc.abstractmethod def clear(self): """Clear database.""" + + @abc.abstractmethod + def record_events(self, events): + """Write the events to the backend storage system. + + :param events: a list of model.Event objects. + """ + + @abc.abstractmethod + def get_events(self, event_filter): + """Return an iterable of model.Event objects. + """ diff --git a/ceilometer/storage/impl_hbase.py b/ceilometer/storage/impl_hbase.py index 3a731f443..0931f1578 100644 --- a/ceilometer/storage/impl_hbase.py +++ b/ceilometer/storage/impl_hbase.py @@ -508,6 +508,20 @@ class Connection(base.Connection): """ raise NotImplementedError('Alarms not implemented') + def record_events(self, events): + """Write the events. + + :param events: a list of model.Event objects. + """ + raise NotImplementedError('Events not implemented.') + + def get_events(self, event_filter): + """Return an iterable of model.Event objects. + + :param event_filter: EventFilter instance + """ + raise NotImplementedError('Events not implemented.') + ############### # This is a very crude version of "in-memory HBase", which implements just diff --git a/ceilometer/storage/impl_log.py b/ceilometer/storage/impl_log.py index 0a6193d6b..d5ae7c39d 100644 --- a/ceilometer/storage/impl_log.py +++ b/ceilometer/storage/impl_log.py @@ -160,3 +160,17 @@ class Connection(base.Connection): def delete_alarm(self, alarm_id): """Delete a alarm """ + + def record_events(self, events): + """Write the events. + + :param events: a list of model.Event objects. + """ + raise NotImplementedError('Events not implemented.') + + def get_events(self, event_filter): + """Return an iterable of model.Event objects. + + :param event_filter: EventFilter instance + """ + raise NotImplementedError('Events not implemented.') diff --git a/ceilometer/storage/impl_mongodb.py b/ceilometer/storage/impl_mongodb.py index 319076b55..7b9be5448 100644 --- a/ceilometer/storage/impl_mongodb.py +++ b/ceilometer/storage/impl_mongodb.py @@ -551,6 +551,20 @@ class Connection(base.Connection): """ self.db.alarm.remove({'alarm_id': alarm_id}) + def record_events(self, events): + """Write the events. + + :param events: a list of model.Event objects. + """ + raise NotImplementedError('Events not implemented.') + + def get_events(self, event_filter): + """Return an iterable of model.Event objects. + + :param event_filter: EventFilter instance + """ + raise NotImplementedError('Events not implemented.') + def require_map_reduce(conn): """Raises SkipTest if the connection is using mim. diff --git a/ceilometer/storage/impl_sqlalchemy.py b/ceilometer/storage/impl_sqlalchemy.py index 224a37b87..d443d2957 100644 --- a/ceilometer/storage/impl_sqlalchemy.py +++ b/ceilometer/storage/impl_sqlalchemy.py @@ -19,10 +19,10 @@ from __future__ import absolute_import +import operator import os import uuid from sqlalchemy import func -from sqlalchemy.orm import exc from ceilometer.openstack.common import log from ceilometer.openstack.common import timeutils @@ -32,6 +32,9 @@ from ceilometer.storage import models as api_models from ceilometer.storage.sqlalchemy import migration from ceilometer.storage.sqlalchemy.models import Meter, Project, Resource from ceilometer.storage.sqlalchemy.models import Source, User, Base, Alarm +from ceilometer.storage.sqlalchemy.models import UniqueName, Event, Trait +from ceilometer import utils + LOG = log.getLogger(__name__) @@ -95,7 +98,7 @@ class SQLAlchemyStorage(base.StorageEngine): def make_query_from_filter(query, sample_filter, require_meter=True): """Return a query dictionary based on the settings in the filter. - :param filter: QueryFilter instance + :param filter: SampleFilter instance :param require_meter: If true and the filter does not have a meter, raise an error. """ @@ -488,3 +491,128 @@ class Connection(base.Connection): """ self.session.query(Alarm).filter(Alarm.id == alarm_id).delete() self.session.flush() + + def _get_unique(self, key): + return self.session.query(UniqueName)\ + .filter(UniqueName.key == key).first() + + def _get_or_create_unique_name(self, key): + """Find the UniqueName entry for a given key, creating + one if necessary. + + This may result in a flush. + """ + unique = self._get_unique(key) + if not unique: + unique = UniqueName(key=key) + self.session.add(unique) + self.session.flush() + return unique + + def _make_trait(self, trait_model, event): + """Make a new Trait from a Trait model. + + Doesn't flush or add to session. + """ + name = self._get_or_create_unique_name(trait_model.name) + value_map = Trait._value_map + values = {'t_string': None, 't_float': None, + 't_int': None, 't_datetime': None} + value = trait_model.value + if trait_model.dtype == api_models.Trait.DATETIME_TYPE: + value = utils.dt_to_decimal(value) + values[value_map[trait_model.dtype]] = value + return Trait(name, event, trait_model.dtype, **values) + + def _record_event(self, event_model): + """Store a single Event, including related Traits. + """ + unique = self._get_or_create_unique_name(event_model.event_name) + + generated = utils.dt_to_decimal(event_model.generated) + event = Event(unique, generated) + self.session.add(event) + + new_traits = [] + if event_model.traits: + for trait in event_model.traits: + t = self._make_trait(trait, event) + self.session.add(t) + new_traits.append(t) + + # Note: we don't flush here, explicitly (unless a new uniquename + # does it). Otherwise, just wait until all the Events are staged. + return (event, new_traits) + + def record_events(self, event_models): + """Write the events to SQL database via sqlalchemy. + + :param event_models: a list of model.Event objects. + + Flush when they're all added, unless new UniqueNames are + added along the way. + """ + events = [self._record_event(event_model) + for event_model in event_models] + + self.session.flush() + + # Update the models with the underlying DB ID. + for model, actual in zip(event_models, events): + actual_event, actual_traits = actual + model.id = actual_event.id + if model.traits and actual_traits: + for trait, actual_trait in zip(model.traits, actual_traits): + trait.id = actual_trait.id + + def get_events(self, event_filter): + """Return an iterable of model.Event objects. + + :param event_filter: EventFilter instance + """ + + start = utils.dt_to_decimal(event_filter.start) + end = utils.dt_to_decimal(event_filter.end) + sub_query = self.session.query(Event.id)\ + .join(Trait, Trait.event_id == Event.id)\ + .filter(Event.generated >= start, Event.generated <= end) + + if event_filter.event_name: + event_name = self._get_unique(event_filter.event_name) + sub_query = sub_query.filter(Event.unique_name == event_name) + + if event_filter.traits: + for key, value in event_filter.traits.iteritems(): + if key == 'key': + key = self._get_unique(value) + sub_query = sub_query.filter(Trait.name == key) + elif key == 't_string': + sub_query = sub_query.filter(Trait.t_string == value) + elif key == 't_int': + sub_query = sub_query.filter(Trait.t_int == value) + elif key == 't_datetime': + dt = utils.dt_to_decimal(value) + sub_query = sub_query.filter(Trait.t_datetime == dt) + elif key == 't_float': + sub_query = sub_query.filter(Trait.t_datetime == value) + + sub_query = sub_query.subquery() + + all_data = self.session.query(Trait)\ + .join(sub_query, Trait.event_id == sub_query.c.id) + + # Now convert the sqlalchemy objects back into Models ... + event_models_dict = {} + for trait in all_data.all(): + event = event_models_dict.get(trait.event_id) + if not event: + generated = utils.decimal_to_dt(trait.event.generated) + event = api_models.Event(trait.event.unique_name.key, + generated, []) + event_models_dict[trait.event_id] = event + value = trait.get_value() + trait_model = api_models.Trait(trait.name.key, trait.t_type, value) + event.append_trait(trait_model) + + event_models = event_models_dict.values() + return sorted(event_models, key=operator.attrgetter('generated')) diff --git a/ceilometer/storage/models.py b/ceilometer/storage/models.py index 0467f1445..33bea9510 100644 --- a/ceilometer/storage/models.py +++ b/ceilometer/storage/models.py @@ -43,6 +43,46 @@ class Model(object): return self.as_dict() == other.as_dict() +class Event(Model): + """A raw event from the source system. Events have Traits. + + Metrics will be derived from one or more Events. + """ + def __init__(self, event_name, generated, traits): + """ + :param event_name: Name of the event. + :param generated: UTC time for when the event occured. + :param traits: list of Traits on this Event. + """ + Model.__init__(self, event_name=event_name, generated=generated, + traits=traits) + + def append_trait(self, trait_model): + self.traits.append(trait_model) + + def __repr__(self): + trait_list = [str(trait) for trait in self.traits] + return "" % \ + (self.event_name, self.generated, " ".join(trait_list)) + + +class Trait(Model): + """A Trait is a key/value pair of data on an Event. The value is variant + record of basic data types (int, date, float, etc). + """ + + TEXT_TYPE = 1 + INT_TYPE = 2 + FLOAT_TYPE = 3 + DATETIME_TYPE = 4 + + def __init__(self, name, dtype, value): + Model.__init__(self, name=name, dtype=dtype, value=value) + + def __repr__(self): + return "" % (self.name, self.dtype, self.value) + + class Resource(Model): """Something for which sample data has been collected. """ diff --git a/ceilometer/storage/sqlalchemy/migrate_repo/versions/008_add_events.py b/ceilometer/storage/sqlalchemy/migrate_repo/versions/008_add_events.py new file mode 100644 index 000000000..8f1fbe239 --- /dev/null +++ b/ceilometer/storage/sqlalchemy/migrate_repo/versions/008_add_events.py @@ -0,0 +1,61 @@ +# -*- encoding: utf-8 -*- +# +# 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 * + +meta = MetaData() + + +unique_name = Table( + 'unique_name', meta, + Column('id', Integer, primary_key=True), + Column('key', String(32), index=True) +) + + +event = Table( + 'event', meta, + Column('id', Integer, primary_key=True), + Column('generated', Float(asdecimal=True), index=True), + Column('unique_name_id', Integer, ForeignKey('unique_name.id')) +) + + +trait = Table( + 'trait', meta, + Column('id', Integer, primary_key=True), + Column('name_id', Integer, ForeignKey('unique_name.id')), + Column('t_type', Integer, index=True), + Column('t_string', String(32), nullable=True, default=None, index=True), + Column('t_float', Float, nullable=True, default=None, index=True), + Column('t_int', Integer, nullable=True, default=None, index=True), + Column('t_datetime', Float(asdecimal=True), nullable=True, default=None, + index=True), + Column('event_id', Integer, ForeignKey('event.id')) +) + + +tables = [unique_name, event, trait] + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + for i in sorted(tables): + i.create() + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + for i in sorted(tables, reverse=True): + i.drop() diff --git a/ceilometer/storage/sqlalchemy/models.py b/ceilometer/storage/sqlalchemy/models.py index eebb11731..6334b2256 100644 --- a/ceilometer/storage/sqlalchemy/models.py +++ b/ceilometer/storage/sqlalchemy/models.py @@ -25,10 +25,13 @@ from oslo.config import cfg from sqlalchemy import Column, Integer, String, Table, ForeignKey, DateTime from sqlalchemy import Float, Boolean, Text from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import backref from sqlalchemy.orm import relationship from sqlalchemy.types import TypeDecorator, VARCHAR from ceilometer.openstack.common import timeutils +from ceilometer.storage import models as api_models +from ceilometer import utils sql_opts = [ cfg.StrOpt('mysql_engine', @@ -174,3 +177,84 @@ class Alarm(Base): insufficient_data_actions = Column(JSONEncodedDict) matching_metadata = Column(JSONEncodedDict) + + +class UniqueName(Base): + """Key names should only be stored once. + """ + __tablename__ = 'unique_name' + id = Column(Integer, primary_key=True) + key = Column(String(32), index=True, unique=True) + + def __init__(self, key): + self.key = key + + def __repr__(self): + return "" % self.key + + +class Event(Base): + __tablename__ = 'event' + id = Column(Integer, primary_key=True) + generated = Column(Float(asdecimal=True), index=True) + + unique_name_id = Column(Integer, ForeignKey('unique_name.id')) + unique_name = relationship("UniqueName", backref=backref('unique_name', + order_by=id)) + + def __init__(self, event, generated): + self.unique_name = event + self.generated = generated + + def __repr__(self): + return "" % \ + (self.id, self.unique_name, self.generated) + + +class Trait(Base): + __tablename__ = 'trait' + id = Column(Integer, primary_key=True) + + name_id = Column(Integer, ForeignKey('unique_name.id')) + name = relationship("UniqueName", backref=backref('name', order_by=id)) + + t_type = Column(Integer, index=True) + t_string = Column(String(32), nullable=True, default=None, index=True) + t_float = Column(Float, nullable=True, default=None, index=True) + t_int = Column(Integer, nullable=True, default=None, index=True) + t_datetime = Column(Float(asdecimal=True), nullable=True, default=None, + index=True) + + event_id = Column(Integer, ForeignKey('event.id')) + event = relationship("Event", backref=backref('event', order_by=id)) + + _value_map = {api_models.Trait.TEXT_TYPE: 't_string', + api_models.Trait.FLOAT_TYPE: 't_float', + api_models.Trait.INT_TYPE: 't_int', + api_models.Trait.DATETIME_TYPE: 't_datetime'} + + def __init__(self, name, event, t_type, t_string=None, t_float=None, + t_int=None, t_datetime=None): + self.name = name + self.t_type = t_type + self.t_string = t_string + self.t_float = t_float + self.t_int = t_int + self.t_datetime = t_datetime + self.event = event + + def get_value(self): + if self.t_type == api_models.Trait.INT_TYPE: + return self.t_int + if self.t_type == api_models.Trait.FLOAT_TYPE: + return self.t_float + if self.t_type == api_models.Trait.DATETIME_TYPE: + return utils.decimal_to_dt(self.t_datetime) + if self.t_type == api_models.Trait.TEXT_TYPE: + return self.t_string + return None + + def __repr__(self): + return "" % (self.name, self.t_type, + self.t_string, self.t_float, self.t_int, self.t_datetime, + self.event) diff --git a/ceilometer/utils.py b/ceilometer/utils.py index f6c676aa0..c50017844 100644 --- a/ceilometer/utils.py +++ b/ceilometer/utils.py @@ -1,9 +1,9 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # Copyright 2011 Justin Santa Barbara + # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -20,9 +20,13 @@ """Utilities and helper functions.""" - +import calendar +import datetime +import decimal import os +from ceilometer.openstack.common import timeutils + def read_cached_file(filename, cache_info, reload_func=None): """Read from a file if it has been modified. @@ -59,3 +63,35 @@ def recursive_keypairs(d): value)) else: yield name, value + + +def dt_to_decimal(utc): + """Datetime to Decimal. + + Some databases don't store microseconds in datetime + so we always store as Decimal unixtime. + """ + decimal.getcontext().prec = 30 + return decimal.Decimal(str(calendar.timegm(utc.utctimetuple()))) + \ + (decimal.Decimal(str(utc.microsecond)) / + decimal.Decimal("1000000.0")) + + +def decimal_to_dt(dec): + """Return a datetime from Decimal unixtime format. + """ + if dec is None: + return None + integer = int(dec) + micro = (dec - decimal.Decimal(integer)) * decimal.Decimal(1000000) + daittyme = datetime.datetime.utcfromtimestamp(integer) + return daittyme.replace(microsecond=int(round(micro))) + + +def sanitize_timestamp(timestamp): + """Return a naive utc datetime object.""" + if not timestamp: + return timestamp + if not isinstance(timestamp, datetime.datetime): + timestamp = timeutils.parse_isotime(timestamp) + return timeutils.normalize_time(timestamp) diff --git a/tests/storage/base.py b/tests/storage/base.py index 0c694cfd3..c9eb2b670 100644 --- a/tests/storage/base.py +++ b/tests/storage/base.py @@ -30,6 +30,7 @@ from ceilometer import counter from ceilometer import storage from ceilometer.tests import db as test_db from ceilometer.storage import models +from ceilometer import utils class DBTestBase(test_db.TestBase): @@ -666,3 +667,144 @@ class AlarmTest(DBTestBase): self.assertEquals(len(survivors), 2) for s in survivors: self.assertNotEquals(victim.name, s.name) + + +class EventTestBase(test_db.TestBase): + """Separate test base class because we don't want to + inherit all the Meter stuff. + """ + __metaclass__ = abc.ABCMeta + + def setUp(self): + super(EventTestBase, self).setUp() + self.prepare_data() + + def prepare_data(self): + # Add some data ... + pass + + +class EventTest(EventTestBase): + def test_save_events_no_traits(self): + now = datetime.datetime.utcnow() + m = [models.Event("Foo", now, None), models.Event("Zoo", now, [])] + self.conn.record_events(m) + for model in m: + self.assertTrue(model.id >= 0) + self.assertNotEqual(m[0].id, m[1].id) + + def test_string_traits(self): + model = models.Trait("Foo", models.Trait.TEXT_TYPE, "my_text") + trait = self.conn._make_trait(model, None) + self.assertEquals(trait.t_type, models.Trait.TEXT_TYPE) + self.assertIsNone(trait.t_float) + self.assertIsNone(trait.t_int) + self.assertIsNone(trait.t_datetime) + self.assertEquals(trait.t_string, "my_text") + self.assertIsNotNone(trait.name) + + def test_int_traits(self): + model = models.Trait("Foo", models.Trait.INT_TYPE, 100) + trait = self.conn._make_trait(model, None) + self.assertEquals(trait.t_type, models.Trait.INT_TYPE) + self.assertIsNone(trait.t_float) + self.assertIsNone(trait.t_string) + self.assertIsNone(trait.t_datetime) + self.assertEquals(trait.t_int, 100) + self.assertIsNotNone(trait.name) + + def test_float_traits(self): + model = models.Trait("Foo", models.Trait.FLOAT_TYPE, 123.456) + trait = self.conn._make_trait(model, None) + self.assertEquals(trait.t_type, models.Trait.FLOAT_TYPE) + self.assertIsNone(trait.t_int) + self.assertIsNone(trait.t_string) + self.assertIsNone(trait.t_datetime) + self.assertEquals(trait.t_float, 123.456) + self.assertIsNotNone(trait.name) + + def test_datetime_traits(self): + now = datetime.datetime.utcnow() + model = models.Trait("Foo", models.Trait.DATETIME_TYPE, now) + trait = self.conn._make_trait(model, None) + self.assertEquals(trait.t_type, models.Trait.DATETIME_TYPE) + self.assertIsNone(trait.t_int) + self.assertIsNone(trait.t_string) + self.assertIsNone(trait.t_float) + self.assertEquals(trait.t_datetime, utils.dt_to_decimal(now)) + self.assertIsNotNone(trait.name) + + def test_save_events_traits(self): + event_models = [] + for event_name in ['Foo', 'Bar', 'Zoo']: + now = datetime.datetime.utcnow() + trait_models = \ + [models.Trait(name, dtype, value) + for name, dtype, value in [ + ('trait_A', models.Trait.TEXT_TYPE, "my_text"), + ('trait_B', models.Trait.INT_TYPE, 199), + ('trait_C', models.Trait.FLOAT_TYPE, 1.23456), + ('trait_D', models.Trait.DATETIME_TYPE, now)]] + event_models.append( + models.Event(event_name, now, trait_models)) + + self.conn.record_events(event_models) + for model in event_models: + for trait in model.traits: + self.assertTrue(trait.id >= 0) + + +class GetEventTest(EventTestBase): + def prepare_data(self): + event_models = [] + base = 0 + self.start = datetime.datetime(2013, 12, 31, 5, 0) + now = self.start + for event_name in ['Foo', 'Bar', 'Zoo']: + trait_models = \ + [models.Trait(name, dtype, value) + for name, dtype, value in [ + ('trait_A', models.Trait.TEXT_TYPE, + "my_%s_text" % event_name), + ('trait_B', models.Trait.INT_TYPE, + base + 1), + ('trait_C', models.Trait.FLOAT_TYPE, + float(base) + 0.123456), + ('trait_D', models.Trait.DATETIME_TYPE, now)]] + event_models.append( + models.Event(event_name, now, trait_models)) + base += 100 + now = now + datetime.timedelta(hours=1) + self.end = now + + self.conn.record_events(event_models) + + def test_simple_get(self): + event_filter = storage.EventFilter(self.start, self.end) + events = self.conn.get_events(event_filter) + self.assertEquals(3, len(events)) + start_time = None + for i, name in enumerate(["Foo", "Bar", "Zoo"]): + self.assertEquals(events[i].event_name, name) + self.assertEquals(4, len(events[i].traits)) + # Ensure sorted results ... + if start_time is not None: + # Python 2.6 has no assertLess :( + self.assertTrue(start_time < events[i].generated) + start_time = events[i].generated + + def test_simple_get_event_name(self): + event_filter = storage.EventFilter(self.start, self.end, "Bar") + events = self.conn.get_events(event_filter) + self.assertEquals(1, len(events)) + self.assertEquals(events[0].event_name, "Bar") + self.assertEquals(4, len(events[0].traits)) + + def test_get_event_trait_filter(self): + trait_filters = {'key': 'trait_B', 't_int': 101} + event_filter = storage.EventFilter(self.start, self.end, + traits=trait_filters) + events = self.conn.get_events(event_filter) + self.assertEquals(1, len(events)) + self.assertEquals(events[0].event_name, "Bar") + self.assertEquals(4, len(events[0].traits)) diff --git a/tests/storage/test_impl_sqlalchemy.py b/tests/storage/test_impl_sqlalchemy.py index 270e0d453..9de03de58 100644 --- a/tests/storage/test_impl_sqlalchemy.py +++ b/tests/storage/test_impl_sqlalchemy.py @@ -25,9 +25,10 @@ from oslo.config import cfg -from tests.storage import base from ceilometer.storage.sqlalchemy.models import table_args +from tests.storage import base + class SQLAlchemyEngineTestBase(base.DBTestBase): database_connection = 'sqlite://' @@ -65,6 +66,37 @@ class AlarmTest(base.AlarmTest, SQLAlchemyEngineTestBase): pass +class EventTestBase(base.EventTestBase): + # Note: Do not derive from SQLAlchemyEngineTestBase, since we + # don't want to automatically inherit all the Meter setup. + database_connection = 'sqlite://' + + +class UniqueNameTest(base.EventTest, EventTestBase): + # UniqueName is a construct specific to sqlalchemy. + # Not applicable to other drivers. + + def test_unique_exists(self): + u1 = self.conn._get_or_create_unique_name("foo") + self.assertTrue(u1.id >= 0) + u2 = self.conn._get_or_create_unique_name("foo") + self.assertEqual(u1, u2) + + def test_new_unique(self): + u1 = self.conn._get_or_create_unique_name("foo") + self.assertTrue(u1.id >= 0) + u2 = self.conn._get_or_create_unique_name("blah") + self.assertNotEqual(u1, u2) + + +class EventTest(base.EventTest, EventTestBase): + pass + + +class GetEventTest(base.GetEventTest, EventTestBase): + pass + + def test_model_table_args(): cfg.CONF.database_connection = 'mysql://localhost' assert table_args() diff --git a/tests/test_utils.py b/tests/test_utils.py index 2d53290a7..4b369d443 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,8 +1,10 @@ # -*- encoding: utf-8 -*- # # Copyright © 2012 New Dream Network, LLC (DreamHost) +# Copyright (c) 2013 OpenStack Foundation # # Author: Doug Hellmann +# 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 @@ -17,7 +19,10 @@ # under the License. """Tests for ceilometer/utils.py """ +import decimal +import datetime +from ceilometer.tests import base as tests_base from ceilometer import utils @@ -34,3 +39,18 @@ def test_recursive_keypairs(): ('nested:a', 'A'), ('nested:b', 'B'), ] + + +class TestUtils(tests_base.TestCase): + def test_datetime_to_decimal(self): + expected = 1356093296.12 + utc_datetime = datetime.datetime.utcfromtimestamp(expected) + actual = utils.dt_to_decimal(utc_datetime) + self.assertEqual(float(actual), expected) + + def test_decimal_to_datetime(self): + expected = 1356093296.12 + dexpected = decimal.Decimal(str(expected)) # Python 2.6 wants str() + expected_datetime = datetime.datetime.utcfromtimestamp(expected) + actual_datetime = utils.decimal_to_dt(dexpected) + self.assertEqual(actual_datetime, expected_datetime)