diff --git a/ceilometer/storage/impl_sqlalchemy.py b/ceilometer/storage/impl_sqlalchemy.py index efd1b0997..fe310fa6c 100644 --- a/ceilometer/storage/impl_sqlalchemy.py +++ b/ceilometer/storage/impl_sqlalchemy.py @@ -19,9 +19,10 @@ from __future__ import absolute_import -import copy 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 @@ -29,7 +30,7 @@ from ceilometer.storage import base 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 +from ceilometer.storage.sqlalchemy.models import Source, User, Base, Alarm import ceilometer.storage.sqlalchemy.session as sqlalchemy_session LOG = log.getLogger(__name__) @@ -412,18 +413,78 @@ class Connection(base.Connection): period_end=period_end, ) + def _row_to_alarm_model(self, row): + return api_models.Alarm(alarm_id=row.id, + enabled=row.enabled, + name=row.name, + description=row.description, + timestamp=row.timestamp, + counter_name=row.counter_name, + user_id=row.user_id, + project_id=row.project_id, + comparison_operator=row.comparison_operator, + threshold=row.threshold, + statistic=row.statistic, + evaluation_periods=row.evaluation_periods, + period=row.period, + state=row.state, + state_timestamp=row.state_timestamp, + ok_actions=row.ok_actions, + alarm_actions=row.alarm_actions, + insufficient_data_actions= + row.insufficient_data_actions, + matching_metadata=row.matching_metadata) + + def _alarm_model_to_row(self, alarm, row=None): + if row is None: + row = Alarm(id=str(uuid.uuid1())) + row.update(alarm.as_dict()) + return row + def get_alarms(self, name=None, user=None, project=None, enabled=True, alarm_id=None): """Yields a lists of alarms that match filters + :param user: Optional ID for user that owns the resource. + :param project: Optional ID for project that owns the resource. + :param enabled: Optional boolean to list disable alarm. + :param alarm_id: Optional alarm_id to return one alarm. """ - raise NotImplementedError('Alarms not implemented') + query = self.session.query(Alarm) + if name is not None: + query = query.filter(Alarm.name == name) + if enabled is not None: + query = query.filter(Alarm.enabled == enabled) + if user is not None: + query = query.filter(Alarm.user_id == user) + if project is not None: + query = query.filter(Alarm.project_id == project) + if alarm_id is not None: + query = query.filter(Alarm.id == alarm_id) + + return (self._row_to_alarm_model(x) for x in query.all()) def update_alarm(self, alarm): """update alarm + + :param alarm: the new Alarm to update """ - raise NotImplementedError('Alarms not implemented') + if alarm.alarm_id: + alarm_row = self.session.merge(Alarm(id=alarm.alarm_id)) + self._alarm_model_to_row(alarm, alarm_row) + else: + self.session.merge(User(id=alarm.user_id)) + self.session.merge(Project(id=alarm.project_id)) + + alarm_row = self._alarm_model_to_row(alarm) + self.session.add(alarm_row) + + self.session.flush() + return self._row_to_alarm_model(alarm_row) def delete_alarm(self, alarm_id): """Delete a alarm + + :param alarm_id: ID of the alarm to delete """ - raise NotImplementedError('Alarms not implemented') + self.session.query(Alarm).filter(Alarm.id == alarm_id).delete() + self.session.flush() diff --git a/ceilometer/storage/sqlalchemy/migrate_repo/versions/007_add_alarm_table.py b/ceilometer/storage/sqlalchemy/migrate_repo/versions/007_add_alarm_table.py new file mode 100644 index 000000000..8ec0d379b --- /dev/null +++ b/ceilometer/storage/sqlalchemy/migrate_repo/versions/007_add_alarm_table.py @@ -0,0 +1,56 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2013 eNovance +# Copyright © 2013 Red Hat, Inc. +# +# Author: Mehdi Abaakouk +# Angus Salkeld +# +# 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 MetaData, Table, Column, Text +from sqlalchemy import Boolean, Integer, String, DateTime, Float + +meta = MetaData() + +alarm = Table( + 'alarm', meta, + Column('id', String(255), primary_key=True, index=True), + Column('enabled', Boolean), + Column('name', Text()), + Column('description', Text()), + Column('timestamp', DateTime(timezone=False)), + Column('counter_name', String(255), index=True), + Column('user_id', String(255), index=True), + Column('project_id', String(255), index=True), + Column('comparison_operator', String(2)), + Column('threshold', Float), + Column('statistic', String(255)), + Column('evaluation_periods', Integer), + Column('period', Integer), + Column('state', String(255)), + Column('state_timestamp', DateTime(timezone=False)), + Column('ok_actions', Text()), + Column('alarm_actions', Text()), + Column('insufficient_data_actions', Text()), + Column('matching_metadata', Text())) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + alarm.create() + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + alarm.drop() diff --git a/ceilometer/storage/sqlalchemy/models.py b/ceilometer/storage/sqlalchemy/models.py index b54507694..eebb11731 100644 --- a/ceilometer/storage/sqlalchemy/models.py +++ b/ceilometer/storage/sqlalchemy/models.py @@ -22,8 +22,8 @@ import json import urlparse from oslo.config import cfg -from sqlalchemy import Column, Integer, String, Table, ForeignKey, DateTime, \ - Float +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 relationship from sqlalchemy.types import TypeDecorator, VARCHAR @@ -74,6 +74,12 @@ class CeilometerBase(object): def __getitem__(self, key): return getattr(self, key) + def update(self, values): + """ Make the model object behave like a dict + """ + for k, v in values.iteritems(): + setattr(self, k, v) + Base = declarative_base(cls=CeilometerBase) @@ -139,3 +145,32 @@ class Resource(Base): user_id = Column(String(255), ForeignKey('user.id')) project_id = Column(String(255), ForeignKey('project.id')) meters = relationship("Meter", backref='resource') + + +class Alarm(Base): + """Alarm data""" + __tablename__ = 'alarm' + id = Column(String(255), primary_key=True) + enabled = Column(Boolean) + name = Column(Text) + description = Column(Text) + timestamp = Column(DateTime, default=timeutils.utcnow) + counter_name = Column(Text) + + user_id = Column(String(255), ForeignKey('user.id')) + project_id = Column(String(255), ForeignKey('project.id')) + + comparison_operator = Column(String(2)) + threshold = Column(Float) + statistic = Column(String(255)) + evaluation_periods = Column(Integer) + period = Column(Integer) + + state = Column(String(255)) + state_timestamp = Column(DateTime, default=timeutils.utcnow) + + ok_actions = Column(JSONEncodedDict) + alarm_actions = Column(JSONEncodedDict) + insufficient_data_actions = Column(JSONEncodedDict) + + matching_metadata = Column(JSONEncodedDict) diff --git a/tests/storage/test_impl_sqlalchemy.py b/tests/storage/test_impl_sqlalchemy.py index 91fbd470b..270e0d453 100644 --- a/tests/storage/test_impl_sqlalchemy.py +++ b/tests/storage/test_impl_sqlalchemy.py @@ -61,6 +61,10 @@ class CounterDataTypeTest(base.CounterDataTypeTest, SQLAlchemyEngineTestBase): pass +class AlarmTest(base.AlarmTest, SQLAlchemyEngineTestBase): + pass + + def test_model_table_args(): cfg.CONF.database_connection = 'mysql://localhost' assert table_args()