check_surykatka_json.py 8.71 KB
Newer Older
1 2 3 4 5 6 7 8 9
from zope.interface import implementer
from slapos.grid.promise import interface
from slapos.grid.promise.generic import GenericPromise

import datetime
import email.utils
import json
import os
import time
10
from six.moves.urllib.parse import urlparse
11 12 13 14


@implementer(interface.IPromise)
class RunPromise(GenericPromise):
15 16 17 18 19 20 21 22
  EXTENDED_STATUS_CODE_MAPPING = {
    '520': 'Too many redirects',
    '523': 'Connection error',
    '524': 'Connection timeout',
    '526': 'SSL Error',

  }

23 24 25 26
  def __init__(self, config):
    super(RunPromise, self).__init__(config)
    # Set frequency compatible to default surykatka interval - 2 minutes
    self.setPeriodicity(float(self.getConfig('frequency', 2)))
27 28 29
    self.error_list = []
    self.info_list = []

30
  def appendErrorMessage(self, message):
31 32
    self.error_list.append(message)

33
  def appendInfoMessage(self, message):
34 35 36 37 38 39 40 41 42
    self.info_list.append(message)

  def emitLog(self):
   if len(self.error_list) > 0:
     emit = self.logger.error
   else:
     emit = self.logger.info

   emit(' '.join(self.error_list + self.info_list))
43 44 45 46

  def senseBotStatus(self):
    key = 'bot_status'

47 48
    def appendError(msg, *args):
      self.appendErrorMessage(key + ': ERROR ' + msg % args)
49 50

    if key not in self.surykatka_json:
51
      appendError("%r not in %r", key, self.json_file)
52 53 54
      return
    bot_status_list = self.surykatka_json[key]
    if len(bot_status_list) == 0:
55
      appendError("%r empty in %r", key, self.json_file)
56 57 58
      return
    bot_status = bot_status_list[0]
    if bot_status.get('text') != 'loop':
59
      appendError("No type loop detected in %r", self.json_file)
60 61 62
      return
    timetuple = email.utils.parsedate(bot_status['date'])
    last_bot_datetime = datetime.datetime.fromtimestamp(time.mktime(timetuple))
63
    last_bot_datetime_string = email.utils.formatdate(time.mktime(timetuple))
64 65 66
    delta = self.utcnow - last_bot_datetime
    # sanity check
    if delta < datetime.timedelta(minutes=0):
67 68
      appendError('Last bot datetime %s is in future, UTC now %s',
                  last_bot_datetime_string, self.utcnow_string)
69 70
      return
    if delta > datetime.timedelta(minutes=15):
71 72 73
      appendError(
        'Last bot datetime %s is more than 15 minutes old, UTC now %s',
        last_bot_datetime_string, self.utcnow_string)
74 75
      return

76 77
    self.appendInfoMessage(
      '%s: OK Last bot status from %s, UTC now is %s' %
78
      (key, last_bot_datetime_string, self.utcnow_string))
79

80 81 82 83
  def senseSslCertificate(self):
    key = 'ssl_certificate'

    def appendError(msg, *args):
84
      self.appendErrorMessage(key + ': ERROR ' + msg % args)
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

    url = self.getConfig('url')
    parsed_url = urlparse(url)
    if parsed_url.scheme == 'https':
      hostname = parsed_url.netloc
      ssl_check = True
      certificate_expiration_days = self.getConfig(
        'certificate-expiration-days', '15')
      try:
        certificate_expiration_days = int(certificate_expiration_days)
      except ValueError:
        certificate_expiration_days = None
    else:
      ssl_check = False
      certificate_expiration_days = None
100
    if not ssl_check:
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
      return
    if certificate_expiration_days is None:
      appendError(
        'certificate-expiration-days %r is incorrect',
        self.getConfig('certificate-expiration-days'))
      return
    if not hostname:
      appendError('url %r is incorrect', url)
      return
    if key not in self.surykatka_json:
      appendError(
        'No data for %s . If the error persist, please update surykatka.', url)
      return
    entry_list = [
      q for q in self.surykatka_json[key] if q['hostname'] == hostname]
    if len(entry_list) == 0:
      appendError('No data for %s', url)
      return
    for entry in entry_list:
      timetuple = email.utils.parsedate(entry['not_after'])
      certificate_expiration_time = datetime.datetime.fromtimestamp(
        time.mktime(timetuple))
      if certificate_expiration_time - datetime.timedelta(
        days=certificate_expiration_days) < self.utcnow:
        appendError(
          'Certificate for %s will expire on %s, which is less than %s days, '
          'UTC now is %s',
128 129
          url, entry['not_after'], certificate_expiration_days,
          self.utcnow_string)
130 131
        return
      else:
132 133
        self.appendInfoMessage(
          '%s: OK Certificate for %s will expire on %s, which is more than %s '
134 135 136 137 138
          'days, UTC now is %s' %
          (key, url, entry['not_after'], certificate_expiration_days,
           self.utcnow_string))
        return

139 140
  def senseHttpQuery(self):
    key = 'http_query'
141
    error = False
142

143 144
    def appendError(msg, *args):
      self.appendErrorMessage(key + ': ERROR ' + msg % args)
145 146

    if key not in self.surykatka_json:
147
      appendError("%r not in %r", key, self.json_file)
148 149 150 151 152 153 154 155
      return

    url = self.getConfig('url')
    status_code = self.getConfig('status-code')
    ip_list = self.getConfig('ip-list', '').split()

    entry_list = [q for q in self.surykatka_json[key] if q['url'] == url]
    if len(entry_list) == 0:
156
      appendError('No data for %s', url)
157 158
      return
    for entry in entry_list:
159 160 161 162 163 164 165 166 167
      entry_status_code = str(entry['status_code'])
      if entry_status_code != status_code:
        status_code_explanation = self.EXTENDED_STATUS_CODE_MAPPING.get(
          entry_status_code)
        if status_code_explanation:
          status_code_explanation = '%s (%s)' % (
            entry_status_code, status_code_explanation)
        else:
          status_code_explanation = entry_status_code
168 169 170 171
        appendError(
          '%s : IP %s got status code %s instead of %s' % (
            url, entry['ip'], status_code_explanation, status_code))
        error = True
172 173 174
    db_ip_list = [q['ip'] for q in entry_list]
    if len(ip_list):
      if set(ip_list) != set(db_ip_list):
175 176 177 178 179
        appendError(
          '%s : expected IPs %s differes from got %s' % (
            url, ' '.join(ip_list), ' '.join(db_ip_list)))
        error = True
    if error:
180 181
      return
    if len(ip_list) > 0:
182 183
      self.appendInfoMessage(
        '%s: OK %s replied correctly with status code %s on ip list %s' %
184
        (key, url, status_code, ' '.join(ip_list)))
185
    else:
186 187
      self.appendInfoMessage(
        '%s: OK %s replied correctly with status code %s' %
188
        (key, url, status_code))
189

190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
  def senseElapsedTime(self):
    key = 'elapsed_time'
    surykatka_key = 'http_query'

    def appendError(msg, *args):
      self.appendErrorMessage(key + ': ERROR ' + msg % args)

    if surykatka_key not in self.surykatka_json:
      appendError("%r not in %r", surykatka_key, self.json_file)
      return

    url = self.getConfig('url')
    maximum_elapsed_time = self.getConfig('maximum-elapsed-time')

    entry_list = [
      q for q in self.surykatka_json[surykatka_key] if q['url'] == url]
    if len(entry_list) == 0:
      appendError('No data for %s', url)
      return
    for entry in entry_list:
      if maximum_elapsed_time:
        if 'total_seconds' in entry:
          maximum_elapsed_time = float(maximum_elapsed_time)
          if entry['total_seconds'] > maximum_elapsed_time:
            appendError(
              '%s : IP %s replied in %.2fs which is longer than '
              'maximum %.2fs' %
              (url, entry['ip'], entry['total_seconds'], maximum_elapsed_time))
          else:
            self.appendInfoMessage(
              '%s: OK %s : IP %s replied in %.2fs which is shorter than '
              'maximum %.2fs' % (key, url, entry['ip'],
                                 entry['total_seconds'], maximum_elapsed_time))

224 225 226 227 228 229 230 231
  def sense(self):
    """
      Check if frontend URL is available
    """
    test_utcnow = self.getConfig('test-utcnow')
    if test_utcnow:
      self.utcnow = datetime.datetime.fromtimestamp(
        time.mktime(email.utils.parsedate(test_utcnow)))
232
      self.utcnow_string = test_utcnow
233 234
    else:
      self.utcnow = datetime.datetime.utcnow()
235 236
      self.utcnow_string = email.utils.formatdate(time.mktime(
        self.utcnow.timetuple()))
237 238 239

    self.json_file = self.getConfig('json-file', '')
    if not os.path.exists(self.json_file):
240
      self.appendErrorMessage('ERROR File %r does not exists' % self.json_file)
241
    else:
242 243 244 245
      with open(self.json_file) as fh:
        try:
          self.surykatka_json = json.load(fh)
        except Exception:
246 247
          self.appendErrorMessage(
            "ERROR loading JSON from %r" % self.json_file)
248 249 250 251 252 253
        else:
          report = self.getConfig('report')
          if report == 'bot_status':
            self.senseBotStatus()
          elif report == 'http_query':
            self.senseHttpQuery()
254
            self.senseSslCertificate()
255
            self.senseElapsedTime()
256
          else:
257 258
            self.appendErrorMessage(
              "ERROR Report %r is not supported" % report)
259
    self.emitLog()
260 261 262

  def anomaly(self):
    return self._test(result_count=3, failure_amount=3)