# # 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 six from bilean.common import exception from bilean.common.i18n import _ from bilean.common.i18n import _LI from bilean.common import utils from bilean.db import api as db_api from bilean.drivers import base as driver_base from bilean import notifier as bilean_notifier from bilean.plugins import base as plugin_base from oslo_config import cfg from oslo_log import log as logging from oslo_utils import timeutils LOG = logging.getLogger(__name__) class User(object): """User object contains all user operations""" statuses = ( INIT, FREE, ACTIVE, WARNING, FREEZE, ) = ( 'INIT', 'FREE', 'ACTIVE', 'WARNING', 'FREEZE', ) ALLOW_DELAY_TIME = 10 def __init__(self, user_id, **kwargs): self.id = user_id self.name = kwargs.get('name') self.policy_id = kwargs.get('policy_id') self.balance = kwargs.get('balance', 0) self.rate = kwargs.get('rate', 0.0) self.credit = kwargs.get('credit', 0) self.last_bill = kwargs.get('last_bill') self.status = kwargs.get('status', self.INIT) self.status_reason = kwargs.get('status_reason', 'Init user') self.created_at = kwargs.get('created_at') self.updated_at = kwargs.get('updated_at') self.deleted_at = kwargs.get('deleted_at') if self.name is None: self.name = self._retrieve_name(self.id) def store(self, context): """Store the user record into database table.""" values = { 'name': self.name, 'policy_id': self.policy_id, 'balance': self.balance, 'rate': self.rate, 'credit': self.credit, 'last_bill': self.last_bill, 'status': self.status, 'status_reason': self.status_reason, 'created_at': self.created_at, 'updated_at': self.updated_at, 'deleted_at': self.deleted_at, } if self.created_at: db_api.user_update(context, self.id, values) else: values.update(id=self.id) user = db_api.user_create(context, values) self.created_at = user.created_at return self.id @classmethod def init_users(cls, context): """Init users from keystone.""" keystoneclient = driver_base.BileanDriver().identity() try: projects = keystoneclient.project_list() except exception.InternalError as ex: LOG.exception(_('Failed in retrieving project list: %s'), six.text_type(ex)) return False users = cls.load_all(context) user_ids = [user.id for user in users] for project in projects: if project.id not in user_ids: user = cls(project.id, name=project.name, status=cls.INIT, status_reason='Init from keystone') user.store(context) users.append(user) return users def _retrieve_name(cls, user_id): '''Get user name form keystone.''' keystoneclient = driver_base.BileanDriver().identity() try: project = keystoneclient.project_find(user_id) except exception.InternalError as ex: LOG.exception(_('Failed in retrieving project: %s'), six.text_type(ex)) return None return project.name @classmethod def _from_db_record(cls, record): '''Construct a user object from database record. :param record: a DB user object that contains all fields; ''' kwargs = { 'name': record.name, 'policy_id': record.policy_id, 'balance': record.balance, 'rate': record.rate, 'credit': record.credit, 'last_bill': record.last_bill, 'status': record.status, 'status_reason': record.status_reason, 'created_at': record.created_at, 'updated_at': record.updated_at, 'deleted_at': record.deleted_at, } return cls(record.id, **kwargs) @classmethod def load(cls, context, user_id=None, user=None, realtime=False, show_deleted=False, project_safe=True): '''Retrieve a user from database.''' if context.is_admin: project_safe = False if user is None: user = db_api.user_get(context, user_id, show_deleted=show_deleted, project_safe=project_safe) if user is None: raise exception.UserNotFound(user=user_id) u = cls._from_db_record(user) if not realtime: return u if u.rate > 0 and u.status != u.FREEZE: seconds = (timeutils.utcnow() - u.last_bill).total_seconds() u.balance -= u.rate * seconds return u @classmethod def load_all(cls, context, show_deleted=False, limit=None, marker=None, sort_keys=None, sort_dir=None, filters=None): '''Retrieve all users of from database.''' records = db_api.user_get_all(context, show_deleted=show_deleted, limit=limit, marker=marker, sort_keys=sort_keys, sort_dir=sort_dir, filters=filters) return [cls._from_db_record(record) for record in records] @classmethod def delete(cls, context, user_id=None, user=None): '''Delete a user from database.''' if user is not None: db_api.user_delete(context, user_id=user.id) return True elif user_id is not None: db_api.user_delete(context, user_id=user_id) return True return False @classmethod def from_dict(cls, values): id = values.pop('id', None) return cls(id, **values) def to_dict(self): user_dict = { 'id': self.id, 'name': self.name, 'policy_id': self.policy_id, 'balance': self.balance, 'rate': self.rate, 'credit': self.credit, 'last_bill': utils.format_time(self.last_bill), 'status': self.status, 'status_reason': self.status_reason, 'created_at': utils.format_time(self.created_at), 'updated_at': utils.format_time(self.updated_at), 'deleted_at': utils.format_time(self.deleted_at), } return user_dict def set_status(self, context, status, reason=None): '''Set status of the user.''' self.status = status if reason: self.status_reason = reason self.store(context) def update_rate(self, context, delta_rate, timestamp=None, delayed_cost=0): """Update user's rate and update user status. :param context: The request context. :param delta_rate: Delta rate to change. :param timestamp: The time that resource action occurs. :param delayed_cost: User's action may be delayed by some reason, adjust balance by delayed_cost. """ if delta_rate == 0 and delayed_cost == 0: return # Settle account before update rate self._settle_account(context, timestamp=timestamp, delayed_cost=delayed_cost) old_rate = self.rate new_rate = old_rate + delta_rate if old_rate == 0 and new_rate > 0: # Set last_bill when status change to 'ACTIVE' from 'FREE' self.last_bill = timeutils.utcnow() reason = _("Status change to 'ACTIVE' cause resource creation.") self.status = self.ACTIVE self.status_reason = reason elif delta_rate < 0: if new_rate == 0 and self.balance >= 0: reason = _("Status change to 'FREE' because of resource " "deletion.") self.status = self.FREE self.status_reason = reason elif self.status == self.WARNING and not self._notify_or_not(): reason = _("Status change from 'WARNING' to 'ACTIVE' " "because of resource deletion.") self.status = self.ACTIVE self.status_reason = reason self.rate = new_rate self.store(context) def do_recharge(self, context, value, recharge_type=None, timestamp=None, metadata=None): """Recharge for user and update status. param context: The request context. param value: Recharge value. param recharge_type: Rechage type, 'Recharge'|'System bonus'. param timestamp: Record when recharge action occurs. param metadata: Some other keyword. """ self.balance += value if self.status == self.INIT and self.balance > 0: self.status = self.FREE self.status_reason = "Recharged" elif self.status == self.FREEZE and self.balance > 0: reason = _("Status change from 'FREEZE' to 'FREE' because " "of recharge.") self.status = self.FREE self.status_reason = reason elif self.status == self.WARNING: if not self._notify_or_not(): reason = _("Status change from 'WARNING' to 'ACTIVE' because " "of recharge.") self.status = self.ACTIVE self.status_reason = reason self.store(context) # Create recharge record values = {'user_id': self.id, 'value': value, 'type': recharge_type, 'timestamp': timestamp, 'metadata': metadata} db_api.recharge_create(context, values) def _notify_or_not(self): '''Check if user should be notified.''' cfg.CONF.import_opt('prior_notify_time', 'bilean.scheduler.cron_scheduler', group='scheduler') prior_notify_time = cfg.CONF.scheduler.prior_notify_time * 3600 rest_usage = prior_notify_time * self.rate if self.balance > rest_usage: return False return True def do_delete(self, context): db_api.user_delete(context, self.id) return True def _settle_account(self, context, timestamp=None, delayed_cost=0): if self.rate == 0 and delayed_cost == 0: LOG.info(_LI("Ignore settlement action because user is in '%s' " "status."), self.status) return # Calculate user's cost before last_bill and now cost = 0 if self.rate > 0 and self.last_bill: timestamp = timestamp or timeutils.utcnow() total_seconds = (timestamp - self.last_bill).total_seconds() cost = self.rate * total_seconds total_cost = cost + delayed_cost self.balance -= total_cost self.last_bill = timestamp def settle_account(self, context, task=None): '''Settle account for user.''' notifier = bilean_notifier.Notifier() self._settle_account(context) if task == 'notify' and self._notify_or_not(): self.status_reason = "The balance is almost used up" self.status = self.WARNING # Notify user msg = {'user': self.id, 'notification': self.status_reason} notifier.info('billing.notify', msg) elif task == 'freeze' and self.balance <= 0: reason = _("Balance overdraft") LOG.info(_LI("Freeze user %(user_id)s, reason: %(reason)s"), {'user_id': self.id, 'reason': reason}) resources = plugin_base.Resource.load_all( context, user_id=self.id, project_safe=False) for resource in resources: resource.do_delete(context) self.rate = 0 self.status = self.FREEZE self.status_reason = reason # Notify user msg = {'user': self.id, 'notification': self.status_reason} notifier.info('billing.notify', msg) self.store(context)