Commit 0e502ce9 authored by Rafael Monnerat's avatar Rafael Monnerat

slapos/grid/promise: Extend promise system to generate history and stats

   This is a port of the code from slapos.toolbox for future
   replacement.
parent ca456d3c
......@@ -38,15 +38,18 @@ import importlib
import traceback
import psutil
import inspect
import hashlib
from datetime import datetime
from multiprocessing import Process, Queue as MQueue
from six.moves import queue, reload_module
from slapos.util import mkdir_p, chownDirectory
from slapos.util import str2bytes, mkdir_p, chownDirectory
from slapos.grid.utils import dropPrivileges, killProcessTree
from slapos.grid.promise import interface
from slapos.grid.promise.generic import (GenericPromise, PromiseQueueResult,
AnomalyResult, TestResult,
PROMISE_STATE_FOLDER_NAME,
PROMISE_RESULT_FOLDER_NAME,
PROMISE_HISTORY_RESULT_FOLDER_NAME,
PROMISE_PARAMETER_NAME)
from slapos.grid.promise.wrapper import WrapPromise
from slapos.version import version
......@@ -342,6 +345,14 @@ class PromiseLauncher(object):
mkdir_p(self.promise_output_dir)
self._updateFolderOwner()
self.promise_history_output_dir = os.path.join(
self.partition_folder,
PROMISE_HISTORY_RESULT_FOLDER_NAME
)
if not os.path.exists(self.promise_history_output_dir):
mkdir_p(self.promise_history_output_dir)
self._updateFolderOwner()
def _generatePromiseResult(self, promise_process, promise_name, promise_path,
message, execution_time=0):
if self.check_anomaly:
......@@ -378,6 +389,81 @@ class PromiseLauncher(object):
json.dump(result.serialize(), outputfile)
os.rename(promise_tmp_file, promise_output_file)
def _savePromiseHistoryResult(self, result):
state_dict = result.serialize()
previous_state_dict = {}
promise_status_file = os.path.join(PROMISE_STATE_FOLDER_NAME,
'promise_status.json')
if os.path.exists(promise_status_file):
with open(promise_status_file) as f:
try:
previous_state_dict = json.load(f)
except ValueError:
pass
history_file = os.path.join(
self.promise_history_output_dir,
'%s.history.json' % result.title
)
# Remove useless informations
result_dict = state_dict.pop('result')
result_dict["change-date"] = result_dict["date"]
state_dict.update(result_dict)
state_dict.pop('path', '')
state_dict.pop('type', '')
state_dict["status"] = "ERROR" if result.item.hasFailed() else "OK"
if not os.path.exists(history_file) or not os.stat(history_file).st_size:
with open(history_file, 'w') as f:
data_dict = {
"date": time.time(),
"data": [state_dict]
}
json.dump(data_dict, f)
else:
previous_state_list = previous_state_dict.get(result.name, None)
if previous_state_list is not None:
_, change_date, checksum = previous_state_list
current_sum = hashlib.md5(str2bytes(state_dict.get('message', ''))).hexdigest()
if state_dict['change-date'] == change_date and \
current_sum == checksum:
# Only save the changes and not the same info
return
state_dict.pop('title', '')
state_dict.pop('name', '')
with open (history_file, mode="r+") as f:
f.seek(0,2)
f.seek(f.tell() -2)
f.write('%s}' % ',{}]'.format(json.dumps(state_dict)))
def _saveStatisticsData(self, stat_file_path, date, success, error):
# csv-like document for success/error statictics
if not os.path.exists(stat_file_path) or os.stat(stat_file_path).st_size == 0:
with open(stat_file_path, 'w') as fstat:
data_dict = {
"date": time.time(),
"data": ["Date, Success, Error, Warning"]
}
fstat.write(json.dumps(data_dict))
current_state = '%s, %s, %s, %s' % (
date,
success,
error,
'')
# append to file
# XXX this is bad, it is getting everywhere.
if current_state:
with open (stat_file_path, mode="r+") as fstat:
fstat.seek(0,2)
position = fstat.tell() -2
fstat.seek(position)
fstat.write('%s}' % ',"{}"]'.format(current_state))
def _loadPromiseResult(self, promise_title):
promise_output_file = os.path.join(
self.promise_output_dir,
......@@ -409,6 +495,7 @@ class PromiseLauncher(object):
self.bang_called = True
# Send result
self._savePromiseResult(result_item)
self._savePromiseHistoryResult(result_item)
def _emptyQueue(self):
"""Remove all entries from queue until it's empty"""
......@@ -565,6 +652,29 @@ class PromiseLauncher(object):
promise_list = []
failed_promise_name = ""
failed_promise_output = ""
previous_state_dict = {}
new_state_dict = {}
report_date = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S+0000')
promise_status_file = os.path.join(self.partition_folder,
PROMISE_STATE_FOLDER_NAME,
'promise_status.json')
promise_result_file = os.path.join(self.partition_folder,
PROMISE_STATE_FOLDER_NAME,
'promise_result.json')
promise_stats_file = os.path.join(self.partition_folder,
PROMISE_STATE_FOLDER_NAME,
'promise_stats.json')
if os.path.exists(promise_status_file):
with open(promise_status_file) as f:
try:
previous_state_dict = json.load(f)
except ValueError:
pass
new_state_dict = previous_state_dict.copy()
base_config = {
'log-folder': self.log_folder,
'partition-folder': self.partition_folder,
......@@ -578,7 +688,8 @@ class PromiseLauncher(object):
'queue': self.queue_result,
'slapgrid-version': version,
}
error = 0
success = 0
if os.path.exists(self.promise_folder) and os.path.isdir(self.promise_folder):
for promise_name in os.listdir(self.promise_folder):
if promise_name.startswith('__init__') or \
......@@ -597,9 +708,36 @@ class PromiseLauncher(object):
config.update(base_config)
promise_result = self._launchPromise(promise_name, promise_path, config)
if promise_result and promise_result.hasFailed() and not failed_promise_name:
failed_promise_name = promise_name
failed_promise_output = promise_result.message
if promise_result:
change_date = promise_result.date.strftime('%Y-%m-%dT%H:%M:%S+0000')
if promise_result.hasFailed():
promise_status = 'FAILED'
error += 1
else:
promise_status = "OK"
success += 1
if promise_name in previous_state_dict:
status, previous_change_date, _ = previous_state_dict[promise_name]
if promise_status == status:
change_date = previous_change_date
message = promise_result.message if promise_result.message else ""
new_state_dict[promise_name] = [
promise_status,
change_date,
hashlib.md5(str2bytes(message)).hexdigest()]
if promise_result.hasFailed() and not failed_promise_name:
failed_promise_name = promise_name
failed_promise_output = promise_result.message
else:
# The promise was skip, so for statistic point of view we preserve
# the previous result
if promise_name in new_state_dict:
if new_state_dict[promise_name][0] == "FAILED":
error += 1
else:
success += 1
if not self.run_only_promise_list and os.path.exists(self.legacy_promise_folder) \
and os.path.isdir(self.legacy_promise_folder):
......@@ -621,11 +759,46 @@ class PromiseLauncher(object):
promise_path,
config,
wrap_process=True)
if promise_result and promise_result.hasFailed() and not failed_promise_name:
failed_promise_name = promise_name
failed_promise_output = promise_result.message
if promise_result:
change_date = promise_result.date.strftime('%Y-%m-%dT%H:%M:%S+0000')
if promise_result.hasFailed():
promise_status = 'FAILED'
error += 1
else:
promise_status = "OK"
success += 1
if promise_name in previous_state_dict:
status, previous_change_date, _ = previous_state_dict[promise_name]
if promise_status == status:
change_date = previous_change_date
message = promise_result.message if promise_result.message else ""
new_state_dict[promise_name] = [
promise_status,
change_date,
hashlib.md5(str2bytes(message)).hexdigest()]
if promise_result.hasFailed() and not failed_promise_name:
failed_promise_name = promise_name
failed_promise_output = promise_result.message
else:
# The promise was skip, so for statistic point of view we preserve
# the previous result
if promise_name in new_state_dict:
if new_state_dict[promise_name][0] == "FAILED":
error += 1
else:
success += 1
self._updateFolderOwner(self.promise_output_dir)
# Save Global State
with open(promise_status_file, "w") as f:
json.dump(new_state_dict, f)
self._saveStatisticsData(promise_stats_file,
report_date, success, error)
if self._skipped_amount > 0:
self.logger.info("%s promises didn't need to be checked." % \
self._skipped_amount)
......@@ -633,3 +806,4 @@ class PromiseLauncher(object):
raise PromiseError("Promise %r failed with output: %s" % (
failed_promise_name,
failed_promise_output))
......@@ -44,7 +44,7 @@ from datetime import datetime, timedelta
PROMISE_STATE_FOLDER_NAME = '.slapgrid/promise'
PROMISE_RESULT_FOLDER_NAME = '.slapgrid/promise/result'
PROMISE_LOG_FOLDER_NAME = '.slapgrid/promise/log'
PROMISE_HISTORY_RESULT_FOLDER_NAME = '.slapgrid/promise/history'
PROMISE_PARAMETER_NAME = 'extra_config_dict'
LOGLINE_RE = r"(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\-?\s*(\w+)\s+\-?\s+(\d+\-\d{3})\s+\-?\s*(.*)"
......
......@@ -40,6 +40,7 @@ from slapos.grid.promise import (interface, PromiseLauncher, PromiseProcess,
PromiseError, PROMISE_CACHE_FOLDER_NAME)
from slapos.grid.promise.generic import (GenericPromise, TestResult, AnomalyResult,
PromiseQueueResult, PROMISE_STATE_FOLDER_NAME,
PROMISE_HISTORY_RESULT_FOLDER_NAME,
PROMISE_RESULT_FOLDER_NAME,
PROMISE_PARAMETER_NAME)
......@@ -170,6 +171,73 @@ class RunPromise(GenericPromise):
with open(os.path.join(self.plugin_dir, name), 'w') as f:
f.write(promise_content)
def assertSuccessResult(self, name):
expected_result = """{
"result": {
"failed": false,
"message": "success",
"type": "Test Result"
},
"path": "%(promise_dir)s/%(name)s.py",
"name": "%(name)s.py",
"execution-time": 0.05,
"title": "%(name)s"
}"""
# first promise
state_file = os.path.join(self.partition_dir, PROMISE_RESULT_FOLDER_NAME, '%s.status.json' % name)
self.assertTrue(os.path.exists(state_file))
with open(state_file) as f:
result_dict = json.loads(f.read())
result_dict['result'].pop('date')
expected_dict = expected_result % {'promise_dir': self.plugin_dir, 'name': name}
self.assertEqual(json.loads(expected_dict), result_dict)
def assertSuccessHistoryResult(self, name, expected_history=None):
if not expected_history:
expected_history = """{
"data": [{
"execution-time": 0.05,
"failed": false,
"message": "success",
"name": "%(name)s.py",
"status": "OK",
"title": "%(name)s"
}]
}"""
history_file = os.path.join(self.partition_dir, PROMISE_HISTORY_RESULT_FOLDER_NAME, '%s.history.json' % name)
self.assertTrue(os.path.exists(history_file))
with open(history_file) as f:
result_dict = json.loads(f.read())
result_dict.pop('date')
for entry in result_dict["data"]:
d = entry.pop("date")
self.assertEqual(d, entry.pop("change-date"))
expected_dict = expected_history % {'name': name}
self.assertEqual(json.loads(expected_dict), result_dict)
def assertSuccessStatsResult(self, success=1, error=0, expected_stats=None):
if not expected_stats:
expected_stats = """{
"data": ["Date, Success, Error, Warning",
"__DATE__, %(success)s, %(error)s, "
]
}"""
stats_file = os.path.join(self.partition_dir, PROMISE_STATE_FOLDER_NAME, 'promise_stats.json')
self.assertTrue(os.path.exists(stats_file))
with open(stats_file) as f:
result_dict = json.loads(f.read())
result_dict.pop('date')
expected_dict = expected_stats % {'success': success, "error": error}
copy = result_dict["data"]
for nline in range(1, len(result_dict["data"])):
line = result_dict["data"][nline]
result_dict["data"][nline] = "__DATE__,%s" % ",".join(line.split(',')[1:])
self.assertEqual(json.loads(expected_dict), result_dict)
class TestSlapOSPromiseLauncher(TestSlapOSPromiseMixin):
......@@ -337,24 +405,10 @@ class RunPromise(GenericPromise):
self.launcher.run()
self.assertTrue(os.path.exists(state_folder))
self.assertTrue(os.path.exists(os.path.join(self.log_dir, 'my_promise.log')))
expected_result = """{
"result": {
"failed": false,
"message": "success",
"type": "Test Result"
},
"path": "%s/my_promise.py",
"name": "my_promise.py",
"execution-time": 0.05,
"title": "my_promise"
}""" % self.plugin_dir
state_file = os.path.join(self.partition_dir, PROMISE_RESULT_FOLDER_NAME, 'my_promise.status.json')
self.assertTrue(os.path.exists(state_file))
with open(state_file) as f:
result_dict = json.loads(f.read())
result_dict['result'].pop('date')
self.assertEqual(json.loads(expected_result), result_dict)
self.assertSuccessResult("my_promise")
self.assertSuccessHistoryResult("my_promise")
self.assertSuccessStatsResult(1)
def test_runpromise_multiple(self):
promise_name = 'my_promise.py'
......@@ -369,35 +423,146 @@ class RunPromise(GenericPromise):
self.assertTrue(os.path.exists(state_folder))
self.assertTrue(os.path.exists(os.path.join(self.log_dir, 'my_promise.log')))
expected_result = """{
"result": {
"failed": false,
"message": "success",
"type": "Test Result"
},
"path": "%(promise_dir)s/%(name)s.py",
"name": "%(name)s.py",
"execution-time": 0.05,
"title": "%(name)s"
}"""
self.assertSuccessResult("my_promise")
self.assertSuccessResult("my_second_promise")
self.assertSuccessHistoryResult("my_promise")
self.assertSuccessHistoryResult("my_second_promise")
self.assertSuccessStatsResult(2)
def test_runpromise_multiple_times_same_promise(self):
promise_name = 'my_promise.py'
self.configureLauncher()
self.generatePromiseScript(promise_name, success=True)
state_folder = os.path.join(self.partition_dir, PROMISE_STATE_FOLDER_NAME)
# run promise will not fail
self.launcher.run()
time.sleep(1)
self.launcher.run()
time.sleep(1)
self.launcher.run()
time.sleep(1)
self.assertTrue(os.path.exists(state_folder))
self.assertTrue(os.path.exists(os.path.join(self.log_dir, 'my_promise.log')))
self.assertSuccessResult("my_promise")
self.assertSuccessHistoryResult("my_promise", expected_history = """{
"data": [{
"execution-time": 0.05,
"failed": false,
"message": "success",
"name": "%(name)s.py",
"status": "OK",
"title": "%(name)s"
},{
"execution-time": 0.05,
"failed": false,
"message": "success",
"status": "OK"
}]
}""")
self.assertSuccessStatsResult(1, expected_stats = """{
"data": ["Date, Success, Error, Warning",
"__DATE__, %(success)s, %(error)s, ",
"__DATE__, %(success)s, %(error)s, ",
"__DATE__, %(success)s, %(error)s, "
]
}""")
def test_runpromise_multiple_times_same_promise_with_failure(self):
promise_name = 'my_promise.py'
self.configureLauncher()
self.generatePromiseScript(promise_name, success=True)
state_folder = os.path.join(self.partition_dir, PROMISE_STATE_FOLDER_NAME)
# run promise will not fail
self.launcher.run()
time.sleep(1)
self.generatePromiseScript(promise_name, success=False)
time.sleep(1)
with self.assertRaises(PromiseError):
self.launcher.run()
time.sleep(1)
with self.assertRaises(PromiseError):
self.launcher.run()
time.sleep(1)
self.assertTrue(os.path.exists(state_folder))
self.assertTrue(os.path.exists(os.path.join(self.log_dir, 'my_promise.log')))
self.assertSuccessHistoryResult("my_promise", expected_history = """{
"data": [{
"execution-time": 0.05,
"failed": false,
"message": "success",
"name": "%(name)s.py",
"status": "OK",
"title": "%(name)s"
},{
"execution-time": 0.05,
"failed": true,
"message": "failed",
"status": "ERROR"
}]
}""")
self.assertSuccessStatsResult(1, expected_stats = """{
"data": ["Date, Success, Error, Warning",
"__DATE__, %(success)s, %(error)s, ",
"__DATE__, 0, 1, ",
"__DATE__, 0, 1, "
]
}""")
def test_runpromise_multiple_times_same_promise_with_flaky_failures(self):
promise_name = 'my_promise.py'
self.configureLauncher()
self.generatePromiseScript(promise_name, success=True)
state_folder = os.path.join(self.partition_dir, PROMISE_STATE_FOLDER_NAME)
# run promise will not fail
self.launcher.run()
time.sleep(1)
self.generatePromiseScript(promise_name, success=False)
time.sleep(1)
with self.assertRaises(PromiseError):
self.launcher.run()
time.sleep(1)
self.generatePromiseScript(promise_name, success=True)
time.sleep(1)
self.launcher.run()
self.assertTrue(os.path.exists(state_folder))
self.assertTrue(os.path.exists(os.path.join(self.log_dir, 'my_promise.log')))
self.maxDiff = None
self.assertSuccessHistoryResult("my_promise", expected_history = """{
"data": [{
"execution-time": 0.05,
"failed": false,
"message": "success",
"name": "%(name)s.py",
"status": "OK",
"title": "%(name)s"
},{
"execution-time": 0.05,
"failed": true,
"message": "failed",
"status": "ERROR"
},{
"execution-time": 0.05,
"failed": false,
"message": "success",
"status": "OK"
}]
}""")
self.assertSuccessStatsResult(1, expected_stats = """{
"data": ["Date, Success, Error, Warning",
"__DATE__, %(success)s, %(error)s, ",
"__DATE__, 0, 1, ",
"__DATE__, %(success)s, %(error)s, "
]
}""")
# first promise
state_file = os.path.join(self.partition_dir, PROMISE_RESULT_FOLDER_NAME, 'my_promise.status.json')
self.assertTrue(os.path.exists(state_file))
with open(state_file) as f:
result_dict = json.loads(f.read())
result_dict['result'].pop('date')
expected_dict = expected_result % {'promise_dir': self.plugin_dir, 'name': 'my_promise'}
self.assertEqual(json.loads(expected_dict), result_dict)
# second promise
state_file = os.path.join(self.partition_dir, PROMISE_RESULT_FOLDER_NAME, 'my_second_promise.status.json')
self.assertTrue(os.path.exists(state_file))
with open(state_file) as f:
result_dict = json.loads(f.read())
result_dict['result'].pop('date')
expected_dict = expected_result % {'promise_dir': self.plugin_dir, 'name': 'my_second_promise'}
self.assertEqual(json.loads(expected_dict), result_dict)
def test_runpromise_no_logdir(self):
promise_name = 'my_promise.py'
......@@ -410,6 +575,8 @@ class RunPromise(GenericPromise):
self.launcher.run()
self.assertTrue(os.path.exists(state_file))
self.assertFalse(os.path.exists(os.path.join(self.log_dir, 'my_promise.log')))
self.assertSuccessHistoryResult("my_promise")
self.assertSuccessStatsResult(1)
def test_runpromise_savemethod(self):
promise_name = 'my_promise.py'
......@@ -433,6 +600,9 @@ class RunPromise(GenericPromise):
self.assertTrue(os.path.exists(state_file))
self.assertTrue(os.path.exists(os.path.join(self.log_dir, 'my_promise.log')))
self.assertSuccessHistoryResult("my_promise")
self.assertSuccessStatsResult(1)
def test_runpromise_savemethod_no_logdir(self):
promise_name = 'my_promise.py'
def test_method(result):
......@@ -455,6 +625,9 @@ class RunPromise(GenericPromise):
self.launcher.run()
self.assertTrue(os.path.exists(state_file))
self.assertFalse(os.path.exists(os.path.join(self.log_dir, 'my_promise.log')))
self.assertSuccessHistoryResult("my_promise")
self.assertSuccessStatsResult(1)
def test_runpromise_savemethod_anomaly(self):
promise_name = 'my_promise.py'
......@@ -477,6 +650,9 @@ class RunPromise(GenericPromise):
self.launcher.run()
self.assertTrue(os.path.exists(state_file))
self.assertTrue(os.path.exists(os.path.join(self.log_dir, 'my_promise.log')))
self.assertSuccessHistoryResult("my_promise")
self.assertSuccessStatsResult(1)
def test_runpromise_savemethod_multiple(self):
promise_name = 'my_promise.py'
......@@ -506,6 +682,19 @@ class RunPromise(GenericPromise):
self.assertTrue(os.path.exists(os.path.join(self.log_dir, 'my_promise.log')))
self.assertTrue(os.path.exists(os.path.join(self.log_dir, 'my_failed_promise.log')))
self.assertSuccessHistoryResult("my_promise")
self.assertSuccessHistoryResult("my_failed_promise", expected_history = """{
"data": [{
"execution-time": 0.05,
"failed": true,
"message": "failed",
"name": "%(name)s.py",
"status": "ERROR",
"title": "%(name)s"
}]
}""")
self.assertSuccessStatsResult(success=1, error=1)
def test_runpromise_savemethod_multiple_success(self):
first_promise = 'my_first_promise.py'
second_promise = 'my_second_promise.py'
......@@ -533,6 +722,12 @@ class RunPromise(GenericPromise):
self.assertTrue(os.path.exists(os.path.join(self.log_dir, 'my_second_promise.log')))
self.assertTrue(os.path.exists(os.path.join(self.log_dir, 'my_third_promise.log')))
self.assertSuccessHistoryResult("my_first_promise")
self.assertSuccessHistoryResult("my_second_promise")
self.assertSuccessHistoryResult("my_third_promise")
self.assertSuccessStatsResult(3)
def test_runpromise_fail_and_success(self):
first_promise = 'my_first_promise.py'
second_promise = 'my_second_promise.py'
......@@ -561,6 +756,12 @@ class RunPromise(GenericPromise):
line = f.readline()
self.assertTrue('success' in line, line)
self.assertSuccessStatsResult(expected_stats = """{
"data": ["Date, Success, Error, Warning",
"__DATE__, 1, 1, ",
"__DATE__, 2, 0, "
]
}""")
def test_runpromise_with_periodicity(self):
first_promise = 'my_first_promise.py'
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment