#
#    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 datetime
from heat.common import exception
from heat.db import api as db_api
from heat.engine import parser
from heat.engine import timestamp
from heat.openstack.common.gettextutils import _
from heat.openstack.common import log as logging
from heat.openstack.common import timeutils
from heat.rpc import api as rpc_api
logger = logging.getLogger(__name__)
[docs]class WatchRule(object):
    WATCH_STATES = (
        ALARM,
        NORMAL,
        NODATA,
        SUSPENDED,
        CEILOMETER_CONTROLLED,
    ) = (
        rpc_api.WATCH_STATE_ALARM,
        rpc_api.WATCH_STATE_OK,
        rpc_api.WATCH_STATE_NODATA,
        rpc_api.WATCH_STATE_SUSPENDED,
        rpc_api.WATCH_STATE_CEILOMETER_CONTROLLED,
    )
    ACTION_MAP = {ALARM: 'AlarmActions',
                  NORMAL: 'OKActions',
                  NODATA: 'InsufficientDataActions'}
    created_at = timestamp.Timestamp(db_api.watch_rule_get, 'created_at')
    updated_at = timestamp.Timestamp(db_api.watch_rule_get, 'updated_at')
    def __init__(self, context, watch_name, rule, stack_id=None,
                 state=NODATA, wid=None, watch_data=[],
                 last_evaluated=timeutils.utcnow()):
        self.context = context
        self.now = timeutils.utcnow()
        self.name = watch_name
        self.state = state
        self.rule = rule
        self.stack_id = stack_id
        period = 0
        if 'Period' in rule:
            period = int(rule['Period'])
        elif 'period' in rule:
            period = int(rule['period'])
        self.timeperiod = datetime.timedelta(seconds=period)
        self.id = wid
        self.watch_data = watch_data
        self.last_evaluated = last_evaluated
    @classmethod
[docs]    def load(cls, context, watch_name=None, watch=None):
        '''
        Load the watchrule object, either by name or via an existing DB object
        '''
        if watch is None:
            try:
                watch = db_api.watch_rule_get_by_name(context, watch_name)
            except Exception as ex:
                logger.warn(_('WatchRule.load (%(watch_name)s) db error '
                            '%(ex)s') % {
                                'watch_name': watch_name, 'ex': ex})
        if watch is None:
            raise exception.WatchRuleNotFound(watch_name=watch_name)
        else:
            return cls(context=context,
                       watch_name=watch.name,
                       rule=watch.rule,
                       stack_id=watch.stack_id,
                       state=watch.state,
                       wid=watch.id,
                       watch_data=watch.watch_data,
                       last_evaluated=watch.last_evaluated)
 
[docs]    def store(self):
        '''
        Store the watchrule in the database and return its ID
        If self.id is set, we update the existing rule
        '''
        wr_values = {
            'name': self.name,
            'rule': self.rule,
            'state': self.state,
            'stack_id': self.stack_id
        }
        if not self.id:
            wr = db_api.watch_rule_create(self.context, wr_values)
            self.id = wr.id
        else:
            db_api.watch_rule_update(self.context, self.id, wr_values)
 
[docs]    def destroy(self):
        '''
        Delete the watchrule from the database.
        '''
        if self.id:
            db_api.watch_rule_delete(self.context, self.id)
 
[docs]    def do_data_cmp(self, data, threshold):
        op = self.rule['ComparisonOperator']
        if op == 'GreaterThanThreshold':
            return data > threshold
        elif op == 'GreaterThanOrEqualToThreshold':
            return data >= threshold
        elif op == 'LessThanThreshold':
            return data < threshold
        elif op == 'LessThanOrEqualToThreshold':
            return data <= threshold
        else:
            return False
 
[docs]    def do_Maximum(self):
        data = 0
        have_data = False
        for d in self.watch_data:
            if d.created_at < self.now - self.timeperiod:
                continue
            if not have_data:
                data = float(d.data[self.rule['MetricName']]['Value'])
                have_data = True
            if float(d.data[self.rule['MetricName']]['Value']) > data:
                data = float(d.data[self.rule['MetricName']]['Value'])
        if not have_data:
            return self.NODATA
        if self.do_data_cmp(data,
                            float(self.rule['Threshold'])):
            return self.ALARM
        else:
            return self.NORMAL
 
[docs]    def do_Minimum(self):
        data = 0
        have_data = False
        for d in self.watch_data:
            if d.created_at < self.now - self.timeperiod:
                continue
            if not have_data:
                data = float(d.data[self.rule['MetricName']]['Value'])
                have_data = True
            elif float(d.data[self.rule['MetricName']]['Value']) < data:
                data = float(d.data[self.rule['MetricName']]['Value'])
        if not have_data:
            return self.NODATA
        if self.do_data_cmp(data,
                            float(self.rule['Threshold'])):
            return self.ALARM
        else:
            return self.NORMAL
 
[docs]    def do_SampleCount(self):
        '''
        count all samples within the specified period
        '''
        data = 0
        for d in self.watch_data:
            if d.created_at < self.now - self.timeperiod:
                continue
            data = data + 1
        if self.do_data_cmp(data,
                            float(self.rule['Threshold'])):
            return self.ALARM
        else:
            return self.NORMAL
 
[docs]    def do_Average(self):
        data = 0
        samples = 0
        for d in self.watch_data:
            if d.created_at < self.now - self.timeperiod:
                continue
            samples = samples + 1
            data = data + float(d.data[self.rule['MetricName']]['Value'])
        if samples == 0:
            return self.NODATA
        data = data / samples
        if self.do_data_cmp(data,
                            float(self.rule['Threshold'])):
            return self.ALARM
        else:
            return self.NORMAL
 
[docs]    def do_Sum(self):
        data = 0
        for d in self.watch_data:
            if d.created_at < self.now - self.timeperiod:
                logger.debug(_('ignoring %s') % str(d.data))
                continue
            data = data + float(d.data[self.rule['MetricName']]['Value'])
        if self.do_data_cmp(data,
                            float(self.rule['Threshold'])):
            return self.ALARM
        else:
            return self.NORMAL
 
[docs]    def get_alarm_state(self):
        fn = getattr(self, 'do_%s' % self.rule['Statistic'])
        return fn()
 
[docs]    def evaluate(self):
        if self.state == self.SUSPENDED:
            return []
        # has enough time progressed to run the rule
        self.now = timeutils.utcnow()
        if self.now < (self.last_evaluated + self.timeperiod):
            return []
        return self.run_rule()
 
[docs]    def get_details(self):
        return {'alarm': self.name,
                'state': self.state}
 
[docs]    def run_rule(self):
        new_state = self.get_alarm_state()
        actions = self.rule_actions(new_state)
        self.state = new_state
        self.last_evaluated = self.now
        self.store()
        return actions
 
[docs]    def rule_actions(self, new_state):
        logger.info(_('WATCH: stack:%(stack)s, watch_name:%(watch_name)s, '
                      'new_state:%(new_state)s'), {'stack': self.stack_id,
                                                   'watch_name': self.name,
                                                   'new_state': new_state})
        actions = []
        if self.ACTION_MAP[new_state] not in self.rule:
            logger.info(_('no action for new state %s'),
                        new_state)
        else:
            s = db_api.stack_get(self.context, self.stack_id)
            stack = parser.Stack.load(self.context, stack=s)
            if (stack.action != stack.DELETE
                    and stack.status == stack.COMPLETE):
                for refid in self.rule[self.ACTION_MAP[new_state]]:
                    actions.append(stack.resource_by_refid(refid).signal)
            else:
                logger.warning(_("Could not process watch state %s for stack")
                               % new_state)
        return actions
 
    def _to_ceilometer(self, data):
        from heat.engine import clients
        clients = clients.Clients(self.context)
        sample = {}
        sample['meter_type'] = 'gauge'
        for k, d in iter(data.items()):
            if k == 'Namespace':
                continue
            sample['meter_name'] = k
            sample['sample_volume'] = d['Value']
            sample['meter_unit'] = d['Unit']
            dims = d.get('Dimensions', {})
            if isinstance(dims, list):
                dims = dims[0]
            sample['resource_metadata'] = dims
            sample['resource_id'] = dims.get('InstanceId')
            logger.debug(_('new sample:%(k)s data:%(sample)s') % {
                         'k': k, 'sample': sample})
            clients.ceilometer().samples.create(**sample)
[docs]    def create_watch_data(self, data):
        if self.state == self.CEILOMETER_CONTROLLED:
            # this is a short term measure for those that have cfn-push-stats
            # within their templates, but want to use Ceilometer alarms.
            self._to_ceilometer(data)
            return
        if self.state == self.SUSPENDED:
            logger.debug(_('Ignoring metric data for %s, SUSPENDED state')
                         % self.name)
            return []
        if self.rule['MetricName'] not in data:
            # Our simplified cloudwatch implementation only expects a single
            # Metric associated with each alarm, but some cfn-push-stats
            # options, e.g --haproxy try to push multiple metrics when we
            # actually only care about one (the one we're alarming on)
            # so just ignore any data which doesn't contain MetricName
            logger.debug(_('Ignoring metric data (only accept %(metric)s) '
                         ': %(data)s') % {
                             'metric': self.rule['MetricName'], 'data': data})
            return
        watch_data = {
            'data': data,
            'watch_rule_id': self.id
        }
        wd = db_api.watch_data_create(None, watch_data)
        logger.debug(_('new watch:%(name)s data:%(data)s')
                     % {'name': self.name, 'data': str(wd.data)})
 
[docs]    def state_set(self, state):
        '''
        Persistently store the watch state
        '''
        if state not in self.WATCH_STATES:
            raise ValueError(_("Invalid watch state %s") % state)
        self.state = state
        self.store()
 
[docs]    def set_watch_state(self, state):
        '''
        Temporarily set the watch state, returns list of functions to be
        scheduled in the stack ThreadGroup for the specified state
        '''
        if state not in self.WATCH_STATES:
            raise ValueError(_('Unknown watch state %s') % state)
        actions = []
        if state != self.state:
            actions = self.rule_actions(state)
            if actions:
                logger.debug(_("Overriding state %(self_state)s for watch "
                             "%(name)s with %(state)s") % {
                                 'self_state': self.state, 'name': self.name,
                                 'state': state})
            else:
                logger.warning(_("Unable to override state %(state)s for "
                               "watch %(name)s") % {
                                   'state': self.state, 'name': self.name})
        return actions
  
[docs]def rule_can_use_sample(wr, stats_data):
    def match_dimesions(rule, data):
        for k, v in iter(rule.items()):
            if k not in data:
                return False
            elif v != data[k]:
                return False
        return True
    if wr.state == WatchRule.SUSPENDED:
        return False
    if wr.state == WatchRule.CEILOMETER_CONTROLLED:
        metric = wr.rule['meter_name']
        rule_dims = {}
        for k, v in iter(wr.rule.get('matching_metadata', {}).items()):
            name = k.split('.')[-1]
            rule_dims[name] = v
    else:
        metric = wr.rule['MetricName']
        rule_dims = dict((d['Name'], d['Value'])
                         for d in wr.rule.get('Dimensions', []))
    if metric not in stats_data:
        return False
    for k, v in iter(stats_data.items()):
        if k == 'Namespace':
            continue
        if k == metric:
            data_dims = v.get('Dimensions', {})
            if isinstance(data_dims, list):
                data_dims = data_dims[0]
            if match_dimesions(rule_dims, data_dims):
                return True
    return False