Commit 151be649 authored by Jérome Perrin's avatar Jérome Perrin

Merge branch 'instance_python_promises' of...

Merge branch 'instance_python_promises' of https://lab.nexedi.com/xavier_thompson/slapos.core into wip/rc
parents 5dd35c05 fbc73362
...@@ -472,6 +472,8 @@ class Partition(object): ...@@ -472,6 +472,8 @@ class Partition(object):
self.instance_min_free_space = instance_min_free_space self.instance_min_free_space = instance_min_free_space
self.instance_python = self.getInstalledPythonExecutable()
def check_free_space(self): def check_free_space(self):
required = self.instance_min_free_space or 0 required = self.instance_min_free_space or 0
...@@ -705,6 +707,7 @@ class Partition(object): ...@@ -705,6 +707,7 @@ class Partition(object):
debug=self.buildout_debug) debug=self.buildout_debug)
self.generateSupervisorConfigurationFile() self.generateSupervisorConfigurationFile()
self.createRetentionLockDelay() self.createRetentionLockDelay()
self.instance_python = self.getInstalledPythonExecutable()
def generateSupervisorConfiguration(self): def generateSupervisorConfiguration(self):
""" """
...@@ -1043,3 +1046,15 @@ class Partition(object): ...@@ -1043,3 +1046,15 @@ class Partition(object):
return float(date_file_path.read()) return float(date_file_path.read())
else: else:
return None return None
def getInstalledPythonExecutable(self):
"""
Return the path of the python executable installed for the SR of this instance.
"""
try:
with open(os.path.join(self.software_path, 'bin', 'buildout')) as f:
shebang = f.readline()
except OSError:
return
if shebang.startswith('#!'):
return shebang[2:].split(None, 1)[0]
...@@ -44,7 +44,7 @@ import hashlib ...@@ -44,7 +44,7 @@ import hashlib
from datetime import datetime from datetime import datetime
from multiprocessing import Process, Queue as MQueue from multiprocessing import Process, Queue as MQueue
from six.moves import queue, reload_module from six.moves import queue, reload_module
from slapos.util import str2bytes, mkdir_p, chownDirectory from slapos.util import str2bytes, mkdir_p, chownDirectory, listifdir
from slapos.grid.utils import dropPrivileges, killProcessTree from slapos.grid.utils import dropPrivileges, killProcessTree
from slapos.grid.promise import interface from slapos.grid.promise import interface
from slapos.grid.promise.generic import (GenericPromise, PromiseQueueResult, from slapos.grid.promise.generic import (GenericPromise, PromiseQueueResult,
...@@ -731,65 +731,63 @@ class PromiseLauncher(object): ...@@ -731,65 +731,63 @@ class PromiseLauncher(object):
error = 0 error = 0
success = 0 success = 0
promise_name_list = [] promise_name_list = []
if os.path.exists(self.promise_folder) and os.path.isdir(self.promise_folder): for promise_name in listifdir(self.promise_folder):
for promise_name in os.listdir(self.promise_folder): if promise_name.endswith(('.pyc', '.pyo')):
for suffix in ['.pyc', '.pyo']:
if promise_name.endswith(suffix):
promise_path = os.path.join(self.promise_folder, promise_name)
if not os.path.exists(promise_path[:-1]):
try:
os.unlink(promise_path)
except Exception as e:
self.logger.warning('Failed to remove %r because of %s', promise_path, e)
else:
self.logger.debug('Removed stale %r', promise_path)
if promise_name.startswith('__init__') or \
not promise_name.endswith('.py'):
continue
promise_name_list.append(promise_name)
if self.run_only_promise_list is not None and not \
promise_name in self.run_only_promise_list:
continue
promise_path = os.path.join(self.promise_folder, promise_name) promise_path = os.path.join(self.promise_folder, promise_name)
config = { if not os.path.exists(promise_path[:-1]):
'path': promise_path, try:
'name': promise_name os.unlink(promise_path)
} except Exception as e:
config.update(base_config) self.logger.warning('Failed to remove %r because of %s', promise_path, e)
promise_result = self._launchPromise(promise_name, promise_path, config) else:
if promise_result: self.logger.debug('Removed stale %r', promise_path)
change_date = promise_result.date.strftime('%Y-%m-%dT%H:%M:%S+0000')
if promise_result.hasFailed(): if promise_name.startswith('__init__') or \
promise_status = 'FAILED' not promise_name.endswith('.py'):
continue
promise_name_list.append(promise_name)
if self.run_only_promise_list is not None and not \
promise_name in self.run_only_promise_list:
continue
promise_path = os.path.join(self.promise_folder, promise_name)
config = {
'path': promise_path,
'name': promise_name
}
config.update(base_config)
promise_result = self._launchPromise(promise_name, promise_path, config)
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 error += 1
else: else:
promise_status = "OK"
success += 1 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 len(promise_name_list) > 0: if not self.run_only_promise_list and len(promise_name_list) > 0:
# cleanup stale json files # cleanup stale json files
...@@ -816,10 +814,9 @@ class PromiseLauncher(object): ...@@ -816,10 +814,9 @@ class PromiseLauncher(object):
if key not in promise_name_list: if key not in promise_name_list:
new_state_dict.pop(key, None) new_state_dict.pop(key, None)
if not self.run_only_promise_list and os.path.exists(self.legacy_promise_folder) \ if not self.run_only_promise_list:
and os.path.isdir(self.legacy_promise_folder):
# run legacy promise styles # run legacy promise styles
for promise_name in os.listdir(self.legacy_promise_folder): for promise_name in listifdir(self.legacy_promise_folder):
promise_path = os.path.join(self.legacy_promise_folder, promise_name) promise_path = os.path.join(self.legacy_promise_folder, promise_name)
if not os.path.isfile(promise_path) or \ if not os.path.isfile(promise_path) or \
not os.access(promise_path, os.X_OK): not os.access(promise_path, os.X_OK):
......
from __future__ import print_function
import argparse
import ast
import os
import sys
# Parse arguments
parser = argparse.ArgumentParser()
parser.add_argument('--promise-folder', required=True)
parser.add_argument('--legacy-promise-folder', default=None)
parser.add_argument('--promise-timeout', type=int, default=20)
parser.add_argument('--partition-folder', default=None)
parser.add_argument('--log-folder', default=None)
parser.add_argument('--force', action='store_true')
parser.add_argument('--check-anomaly', action='store_true')
parser.add_argument('--debug', action='store_true')
parser.add_argument('--master-url', default=None)
parser.add_argument('--partition-cert', default=None)
parser.add_argument('--partition-key', default=None)
parser.add_argument('--partition-id', default=None)
parser.add_argument('--computer-id', default=None)
args = parser.parse_args()
# Extract slapos.core path and all dependencies from first promise found
# to import slapos.core
promise_folder = args.promise_folder
promise_file = next(
p for p in os.listdir(promise_folder)
if p.endswith('.py') and not p.startswith('__init__')
)
with open(os.path.join(promise_folder, promise_file)) as f:
promise_content = f.read()
tree = ast.parse(promise_content, mode='exec')
sys.path[0:0] = eval(compile(ast.Expression(tree.body[1].value), '', 'eval'))
from slapos.grid.promise import PromiseLauncher, PromiseError
from slapos.cli.entry import SlapOSApp
# Configure promise launcher
# with the same logger as standard slapos command
app = SlapOSApp()
app.options, _ = app.parser.parse_known_args([])
app.configure_logging()
config = {k.replace('_', '-') : v for k, v in vars(args).items()}
promise_checker = PromiseLauncher(config=config, logger=app.log)
# Run promises
# Redirect stdout to stderr (logger only uses stderr already)
# to reserve stdout for error reporting
out = os.dup(1)
os.dup2(2, 1)
try:
promise_checker.run()
except Exception as e:
os.write(out, str(e))
sys.exit(2 if isinstance(e, PromiseError) else 1)
This diff is collapsed.
...@@ -45,6 +45,7 @@ import json ...@@ -45,6 +45,7 @@ import json
import re import re
import grp import grp
import hashlib import hashlib
import errno
import mock import mock
from mock import patch from mock import patch
...@@ -65,6 +66,11 @@ import slapos.grid.SlapObject ...@@ -65,6 +66,11 @@ import slapos.grid.SlapObject
from slapos import manager as slapmanager from slapos import manager as slapmanager
from slapos.util import dumps from slapos.util import dumps
from slapos import __path__ as slapos_path
from zope import __path__ as zope_path
PROMISE_PATHS = sorted(set(map(os.path.dirname, slapos_path + zope_path)))
import httmock import httmock
...@@ -113,6 +119,9 @@ touch worked ...@@ -113,6 +119,9 @@ touch worked
""" """
PROMISE_CONTENT_TEMPLATE = """ PROMISE_CONTENT_TEMPLATE = """
import sys
sys.path[0:0] = %(paths)r
from zope.interface import implementer from zope.interface import implementer
from slapos.grid.promise import interface from slapos.grid.promise import interface
from slapos.grid.promise import GenericPromise from slapos.grid.promise import GenericPromise
...@@ -122,7 +131,7 @@ class RunPromise(GenericPromise): ...@@ -122,7 +131,7 @@ class RunPromise(GenericPromise):
def __init__(self, config): def __init__(self, config):
super(RunPromise, self).__init__(config) super(RunPromise, self).__init__(config)
self.setPeriodicity(minute=%(periodicity)s) self.setPeriodicity(minute=%(periodicity)r)
def sense(self): def sense(self):
%(content)s %(content)s
...@@ -133,11 +142,11 @@ class RunPromise(GenericPromise): ...@@ -133,11 +142,11 @@ class RunPromise(GenericPromise):
self.logger.info("success") self.logger.info("success")
def anomaly(self): def anomaly(self):
return self._anomaly(result_count=2, failure_amount=%(failure_amount)s) return self._anomaly(result_count=2, failure_amount=%(failure_amount)r)
def test(self): def test(self):
return self._test(result_count=1, failure_amount=%(failure_amount)s) return self._test(result_count=1, failure_amount=%(failure_amount)r)
""" """
class BasicMixin(object): class BasicMixin(object):
def setUp(self): def setUp(self):
...@@ -150,6 +159,14 @@ class BasicMixin(object): ...@@ -150,6 +159,14 @@ class BasicMixin(object):
del os.environ['SLAPGRID_INSTANCE_ROOT'] del os.environ['SLAPGRID_INSTANCE_ROOT']
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
self.setSlapgrid() self.setSlapgrid()
self.setMock()
def setMock(self):
cls = SlapObject.Partition
attr = 'getInstalledPythonExecutable'
orig = getattr(cls, attr)
setattr(cls, attr, lambda self: None)
self.addCleanup(setattr, cls, attr, orig)
def setSlapgrid(self, develop=False, force_stop=False): def setSlapgrid(self, develop=False, force_stop=False):
if getattr(self, 'master_url', None) is None: if getattr(self, 'master_url', None) is None:
...@@ -576,7 +593,8 @@ class InstanceForTest(object): ...@@ -576,7 +593,8 @@ class InstanceForTest(object):
{'success': success, {'success': success,
'content': promise_content, 'content': promise_content,
'failure_amount': failure_count, 'failure_amount': failure_count,
'periodicity': periodicity} 'periodicity': periodicity,
'paths': PROMISE_PATHS}
with open(os.path.join(promise_dir, promise_name), 'w') as f: with open(os.path.join(promise_dir, promise_name), 'w') as f:
f.write(_promise_content) f.write(_promise_content)
...@@ -599,7 +617,7 @@ class InstanceForTest(object): ...@@ -599,7 +617,7 @@ class InstanceForTest(object):
class SoftwareForTest(object): class SoftwareForTest(object):
""" """
Class to prepare and simulate software. Class to prepare and simulate software.
each instance has a sotfware attributed each instance has a software attributed
""" """
def __init__(self, software_root, name=''): def __init__(self, software_root, name=''):
""" """
...@@ -2297,6 +2315,7 @@ exit 1 ...@@ -2297,6 +2315,7 @@ exit 1
self.assertFalse(os.path.exists(promise_file)) self.assertFalse(os.path.exists(promise_file))
self.assertTrue(instance.error) self.assertTrue(instance.error)
class TestSlapgridDestructionLock(MasterMixin, unittest.TestCase): class TestSlapgridDestructionLock(MasterMixin, unittest.TestCase):
def test_retention_lock(self): def test_retention_lock(self):
""" """
...@@ -3999,9 +4018,6 @@ class TestSlapgridPromiseWithMaster(MasterMixin, unittest.TestCase): ...@@ -3999,9 +4018,6 @@ class TestSlapgridPromiseWithMaster(MasterMixin, unittest.TestCase):
self.assertEqual('success', result["result"]["message"]) self.assertEqual('success', result["result"]["message"])
def test_one_succeeding_one_timing_out_promises(self): def test_one_succeeding_one_timing_out_promises(self):
computer = ComputerForTest(self.software_root, self.instance_root) computer = ComputerForTest(self.software_root, self.instance_root)
with httmock.HTTMock(computer.request_handler): with httmock.HTTMock(computer.request_handler):
...@@ -4095,6 +4111,58 @@ class TestSlapgridPromiseWithMaster(MasterMixin, unittest.TestCase): ...@@ -4095,6 +4111,58 @@ class TestSlapgridPromiseWithMaster(MasterMixin, unittest.TestCase):
".slapgrid/promise/result/fail.status.json"))) ".slapgrid/promise/result/fail.status.json")))
class TestSlapgridPluginPromiseWithInstancePython(TestSlapgridPromiseWithMaster):
expect_plugin = False
def setPython(self):
self.python_called = os.path.join(self.software_root, 'called')
wrapper = """#!/bin/sh
touch %s
exec %s "$@"
""" % (self.python_called, sys.executable)
path = os.path.join(self.software_root, 'python')
with open(path, 'w') as f:
f.write(wrapper)
os.chmod(path, 0o755)
return path
def patchBuildoutSetter(self):
cls = SoftwareForTest
attr = 'setBuildout'
orig = getattr(cls, attr)
def setBuildout(soft):
buildout = "#!" + self.setPython()
orig(soft, buildout)
setattr(cls, attr, setBuildout)
self.addCleanup(setattr, cls, attr, orig)
def patchPluginSetter(self):
cls = InstanceForTest
attr = 'setPluginPromise'
orig = getattr(cls, attr)
def setPluginPromise(inst, *args, **kwargs):
self.expect_plugin = inst.requested_state == 'started'
return orig(inst, *args, **kwargs)
setattr(cls, attr, setPluginPromise)
self.addCleanup(setattr, cls, attr, orig)
def setMock(self):
self.patchBuildoutSetter()
self.patchPluginSetter()
def tearDown(self):
try:
os.remove(self.python_called)
called = True
except OSError as e:
if e.errno != errno.ENOENT:
raise
called = False
finally:
super(TestSlapgridPluginPromiseWithInstancePython, self).tearDown()
self.assertEqual(self.expect_plugin, called)
class TestSVCBackend(unittest.TestCase): class TestSVCBackend(unittest.TestCase):
"""Tests for supervisor backend. """Tests for supervisor backend.
""" """
......
...@@ -108,6 +108,18 @@ def mkdir_p(path, mode=0o700): ...@@ -108,6 +108,18 @@ def mkdir_p(path, mode=0o700):
raise raise
def listifdir(path):
"""
Like listdir, but returns an empty tuple if the path is not a directory.
"""
try:
return os.listdir(path)
except OSError as e:
if e.errno != errno.ENOENT:
raise
return ()
def chownDirectory(path, uid, gid): def chownDirectory(path, uid, gid):
if os.getuid() != 0: if os.getuid() != 0:
# we are probably inside of a webrunner # we are probably inside of a webrunner
......
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