diff --git a/cloudkitty/db/__init__.py b/cloudkitty/db/__init__.py new file mode 100644 index 00000000..38947a7a --- /dev/null +++ b/cloudkitty/db/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 Objectif Libre +# +# 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. +# +# @author: Stéphane Albert +# +from oslo.config import cfg +from oslo.db.sqlalchemy import session + +_FACADE = None + + +def _create_facade_lazily(): + global _FACADE + if _FACADE is None: + _FACADE = session.EngineFacade.from_config(cfg.CONF) + return _FACADE + + +def get_engine(): + facade = _create_facade_lazily() + return facade.get_engine() + + +def get_session(**kwargs): + facade = _create_facade_lazily() + return facade.get_session(**kwargs) diff --git a/cloudkitty/db/api.py b/cloudkitty/db/api.py new file mode 100644 index 00000000..ad638962 --- /dev/null +++ b/cloudkitty/db/api.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 Objectif Libre +# +# 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. +# +# @author: Stéphane Albert +# +import abc + +from oslo.config import cfg +from oslo.db import api as db_api +import six + +_BACKEND_MAPPING = {'sqlalchemy': 'cloudkitty.db.sqlalchemy.api'} +IMPL = db_api.DBAPI.from_config(cfg.CONF, + backend_mapping=_BACKEND_MAPPING, + lazy=True) + + +def get_instance(): + """Return a DB API instance.""" + return IMPL + + +@six.add_metaclass(abc.ABCMeta) +class State(object): + """Base class for state tracking.""" + + @abc.abstractmethod + def get_state(self, name): + """Retrieve the current state. + + :param name: Name of the state + :return float: State value + """ + + @abc.abstractmethod + def set_state(self, name, state): + """Store the state. + + :param name: Name of the state + :param state: State value + """ + + @abc.abstractmethod + def get_metadata(self, name): + """Retrieve state metadata + + :param name: Name of the state + :return str: Return a json dict with all metadata attached to this + state. + """ + + @abc.abstractmethod + def set_metadata(self, name, metadata): + """Store the state metadata. + + :param name: Name of the state + :param metadata: Metadata value + """ diff --git a/cloudkitty/db/sqlalchemy/__init__.py b/cloudkitty/db/sqlalchemy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cloudkitty/db/sqlalchemy/api.py b/cloudkitty/db/sqlalchemy/api.py new file mode 100644 index 00000000..7690f17f --- /dev/null +++ b/cloudkitty/db/sqlalchemy/api.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 Objectif Libre +# +# 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. +# +# @author: Stéphane Albert +# +from oslo.config import cfg +from oslo.db.sqlalchemy import utils +import sqlalchemy + +from cloudkitty import config # NOQA +from cloudkitty import db +from cloudkitty.db import api +from cloudkitty.db.sqlalchemy import models +from cloudkitty.openstack.common import log as logging + +CONF = cfg.CONF + +LOG = logging.getLogger(__name__) + + +def get_backend(): + return DBAPIManager + + +class State(api.State): + + def get_state(self, name): + session = db.get_session() + try: + return bool(utils.model_query( + models.StateInfo, + session + ).filter_by( + name=name, + ).value('state')) + except sqlalchemy.orm.exc.NoResultFound: + return None + + def set_state(self, name, state): + session = db.get_session() + with session.begin(): + try: + q = utils.model_query( + models.StateInfo, + session + ).filter_by( + name=name, + ).with_lockmode('update') + db_state = q.one() + db_state.state = state + except sqlalchemy.orm.exc.NoResultFound: + db_state = models.StateInfo(name=name, state=state) + session.add(db_state) + return bool(db_state.state) + + def get_metadata(self, name): + session = db.get_session() + return utils.model_query( + models.StateInfo, + session + ).filter_by( + name=name, + ).value('s_metadata') + + def set_metadata(self, name, metadata): + session = db.get_session() + try: + db_state = utils.model_query( + models.StateInfo, + session + ).filter_by( + name=name, + ).with_lockmode('update').one() + db_state.s_metadata = metadata + except sqlalchemy.orm.exc.NoResultFound: + db_state = models.StateInfo(name=name, s_metadata=metadata) + session.add(db_state) + finally: + session.flush() + + +class DBAPIManager(object): + + @staticmethod + def get_state(): + return State() diff --git a/cloudkitty/db/sqlalchemy/models.py b/cloudkitty/db/sqlalchemy/models.py new file mode 100644 index 00000000..6e1c8789 --- /dev/null +++ b/cloudkitty/db/sqlalchemy/models.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 Objectif Libre +# +# 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. +# +# @author: Stéphane Albert +# +from oslo.db.sqlalchemy import models +import sqlalchemy +from sqlalchemy.ext import declarative + + +Base = declarative.declarative_base() + + +class StateInfo(Base, models.ModelBase): + """State + + """ + + __tablename__ = 'states' + + name = sqlalchemy.Column(sqlalchemy.String(255), + primary_key=True) + state = sqlalchemy.Column( + sqlalchemy.Float(), + nullable=False) + s_metadata = sqlalchemy.Column(sqlalchemy.Text(), + nullable=True, + default='') + + def __repr__(self): + return ('').format( + name=self.name, + state=self.state, + metadata=self.s_metadata) diff --git a/cloudkitty/orchestrator.py b/cloudkitty/orchestrator.py index c28d2bfb..d8067d93 100644 --- a/cloudkitty/orchestrator.py +++ b/cloudkitty/orchestrator.py @@ -49,10 +49,8 @@ class Orchestrator(object): auth_url=CONF.auth.url) s_backend = i_utils.import_class(CONF.state.backend) - self.sm = state.StateManager(s_backend, - CONF.state.basepath, - self.keystone.user_id, - 'osrtf') + self.sm = state.DBStateManager(self.keystone.user_id, + 'osrtf') collector_args = {'user': CONF.auth.username, 'password': CONF.auth.password, diff --git a/cloudkitty/state.py b/cloudkitty/state.py index 8cb85cc8..e71c6e07 100644 --- a/cloudkitty/state.py +++ b/cloudkitty/state.py @@ -17,6 +17,8 @@ # import json +from cloudkitty.db import api + class StateManager(object): def __init__(self, state_backend, state_basepath, user_id, report_type, @@ -31,8 +33,13 @@ class StateManager(object): self._ts = None self._metadata = {} + # Load states + self._load() + def _gen_filename(self): - filename = '{}_{}.state'.format(self._type, self._uid) + # FIXME(sheeprine): Basepath can't be enforced at the moment + filename = '{}_{}.state'.format(self._type, + self._uid) return filename def _open(self, mode='rb'): @@ -43,9 +50,11 @@ class StateManager(object): def _load(self): try: state_file = self._open() - state_data = json.loads(state_file.read()) - self._ts = state_data['timestamp'] - self._metadata = state_data['metadata'] + raw_data = state_file.read() + if raw_data: + state_data = json.loads(raw_data) + self._ts = state_data['timestamp'] + self._metadata = state_data['metadata'] state_file.close() except IOError: pass @@ -82,3 +91,35 @@ class StateManager(object): if self._distributed: self._load() return self._metadata + + +class DBStateManager(object): + def __init__(self, user_id, report_type, distributed=False): + self._state_name = self._gen_name(report_type, user_id) + self._distributed = distributed + self._db = api.get_instance().get_state() + + def _gen_name(self, state_type, uid): + name = '{}_{}'.format(state_type, uid) + return name + + def get_state(self): + """Get the state timestamp.""" + + return self._db.get_state(self._state_name) + + def set_state(self, timestamp): + """Set the current state's timestamp.""" + + self._db.set_state(self._state_name, timestamp) + + def get_metadata(self): + """Get metadata attached to the state.""" + + return json.loads(self._db.get_metadata(self._state_name)) + + def set_metadata(self, metadata): + """Set metadata attached to the state.""" + + self._db.set_metadata(self._state_name, + json.dumps(metadata)) diff --git a/cloudkitty/writer/__init__.py b/cloudkitty/writer/__init__.py index dbb4f6a2..5fafb493 100644 --- a/cloudkitty/writer/__init__.py +++ b/cloudkitty/writer/__init__.py @@ -32,10 +32,8 @@ class BaseReportWriter(object): self._write_orchestrator = write_orchestrator self._write_backend = backend self._uid = user_id - self._sm = state.StateManager(state_backend, - None, - self._uid, - self.report_type) + self._sm = state.DBStateManager(self._uid, + self.report_type) self._report = None self._period = 3600