standalone: initial implementation
A set of utility classes to run a local slapos computer and control softwares/instances on this computer
Showing
slapos/slap/standalone.py
0 → 100644
# -*- coding: utf-8 -*- | |||
############################################################################## | |||
# | |||
# Copyright (c) 2018 Vifib SARL and Contributors. All Rights Reserved. | |||
# | |||
# WARNING: This program as such is intended to be used by professional | |||
# programmers who take the whole responsibility of assessing all potential | |||
# consequences resulting from its eventual inadequacies and bugs | |||
# End users who are looking for a ready-to-use solution with commercial | |||
# guarantees and support are strongly adviced to contract a Free Software | |||
# Service Company | |||
# | |||
# This program is Free Software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
# as published by the Free Software Foundation; either version 3 | |||
# of the License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |||
# | |||
############################################################################## | |||
import glob | |||
import os | |||
import textwrap | |||
import logging | |||
import time | |||
import errno | |||
import socket | |||
import shutil | |||
from six.moves import urllib | |||
from six.moves import http_client | |||
try: | |||
import subprocess32 as subprocess | |||
except ImportError: | |||
import subprocess | |||
import xml_marshaller | |||
import zope.interface | |||
import psutil | |||
from .interface.slap import IException | |||
from .interface.slap import ISupply | |||
from .interface.slap import IRequester | |||
from .slap import slap | |||
from ..grid.svcbackend import getSupervisorRPC | |||
@zope.interface.implementer(IException) | |||
class SlapOSNodeCommandError(Exception): | |||
"""Exception raised when running a SlapOS Node command failed. | |||
""" | |||
def __str__(self): | |||
return "{} exitstatus: {} output:\n{}".format( | |||
self.__class__.__name__, | |||
self.args[0]['exitstatus'], | |||
self.args[0]['output'], | |||
) | |||
@zope.interface.implementer(IException) | |||
class PathTooDeepError(Exception): | |||
"""Exception raised when path is too deep to create an unix socket. | |||
""" | |||
class ConfigWriter(object): | |||
"""Base class for an object writing a config file or wrapper script. | |||
""" | |||
def __init__(self, standalone_slapos): | |||
self._standalone_slapos = standalone_slapos | |||
def writeConfig(self, path): | |||
NotImplemented | |||
class SupervisorConfigWriter(ConfigWriter): | |||
"""Write supervisor configuration at etc/supervisor.conf | |||
""" | |||
def _getProgramConfig(self, program_name, command, stdout_logfile): | |||
"""Format a supervisor program block. | |||
""" | |||
return textwrap.dedent( | |||
"""\ | |||
[program:{program_name}] | |||
command = {command} | |||
autostart = false | |||
autorestart = false | |||
startretries = 0 | |||
startsecs = 0 | |||
redirect_stderr = true | |||
stdout_logfile = {stdout_logfile} | |||
stdout_logfile_maxbytes = 5MB | |||
stdout_logfile_backups = 10 | |||
""".format(**locals())) | |||
def _getSupervisorConfigParts(self): | |||
"""Iterator on parts of formatted config. | |||
""" | |||
standalone_slapos = self._standalone_slapos | |||
yield textwrap.dedent( | |||
""" | |||
[unix_http_server] | |||
file = {standalone_slapos._supervisor_socket} | |||
[supervisorctl] | |||
serverurl = unix://{standalone_slapos._supervisor_socket} | |||
[supervisord] | |||
logfile = {standalone_slapos._supervisor_log} | |||
pidfile = {standalone_slapos._supervisor_pid} | |||
childlogdir = {standalone_slapos._log_directory} | |||
[rpcinterface:supervisor] | |||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface | |||
[program:slapos-proxy] | |||
command = slapos proxy start --cfg {standalone_slapos._slapos_config} --verbose | |||
startretries = 0 | |||
startsecs = 0 | |||
redirect_stderr = true | |||
""".format(**locals())) | |||
for program, program_config in standalone_slapos._slapos_commands.items(): | |||
yield self._getProgramConfig( | |||
program, | |||
program_config['command'].format( | |||
self=standalone_slapos, debug_args=''), | |||
stdout_logfile=program_config.get( | |||
'stdout_logfile', 'AUTO').format(self=standalone_slapos)) | |||
def writeConfig(self, path): | |||
with open(path, 'w') as f: | |||
for part in self._getSupervisorConfigParts(): | |||
f.write(part) | |||
class SlapOSConfigWriter(ConfigWriter): | |||
"""Write slapos configuration at etc/slapos.cfg | |||
""" | |||
def writeConfig(self, path): | |||
standalone_slapos = self._standalone_slapos # type: StandaloneSlapOS | |||
read_only_shared_part_list = '\n '.join( # pylint: disable=unused-variable; used in format() | |||
standalone_slapos._shared_part_list) | |||
with open(path, 'w') as f: | |||
f.write( | |||
textwrap.dedent( | |||
""" | |||
[slapos] | |||
software_root = {standalone_slapos._software_root} | |||
instance_root = {standalone_slapos._instance_root} | |||
shared_part_list = | |||
{read_only_shared_part_list} | |||
{standalone_slapos._shared_part_root} | |||
master_url = {standalone_slapos._master_url} | |||
computer_id = {standalone_slapos._computer_id} | |||
root_check = False | |||
pidfile_software = {standalone_slapos._instance_pid} | |||
pidfile_instance = {standalone_slapos._software_pid} | |||
pidfile_report = {standalone_slapos._report_pid} | |||
[slapproxy] | |||
host = {standalone_slapos._server_ip} | |||
port = {standalone_slapos._server_port} | |||
database_uri = {standalone_slapos._proxy_database} | |||
""".format(**locals()))) | |||
class SlapOSCommandWriter(ConfigWriter): | |||
"""Write a bin/slapos wrapper. | |||
""" | |||
def writeConfig(self, path): | |||
with open(path, 'w') as f: | |||
f.write( | |||
textwrap.dedent( | |||
"""\ | |||
#!/bin/sh | |||
SLAPOS_CONFIGURATION={self._standalone_slapos._slapos_config} \\ | |||
SLAPOS_CLIENT_CONFIGURATION=$SLAPOS_CONFIGURATION \\ | |||
exec slapos "$@" | |||
""".format(**locals()))) | |||
os.chmod(path, 0o755) | |||
@zope.interface.implementer(ISupply, IRequester) | |||
class StandaloneSlapOS(object): | |||
"""A SlapOS that can be embedded in other applications, also useful for testing. | |||
This plays the role of an `IComputer` where users of classes implementing this | |||
interface can install software, create partitions and access parameters of the | |||
running partitions. | |||
Extends the existing `IRequester` and `ISupply`, with the special behavior that | |||
`IRequester.request` and `ISupply.supply` will only use the embedded computer. | |||
""" | |||
def __init__( | |||
self, | |||
base_directory, | |||
server_ip, | |||
server_port, | |||
computer_id='local', | |||
shared_part_list=(), | |||
software_root=None, | |||
instance_root=None, | |||
shared_part_root=None): | |||
"""Constructor, creates a standalone slapos in `base_directory`. | |||
Arguments: | |||
* `base_directory` -- the directory which will contain softwares and instances. | |||
* `server_ip`, `server_port` -- the address this SlapOS proxy will listen to. | |||
* `computer_id` -- the id of this computer. | |||
* `shared_part_list` -- list of extra paths to use as read-only ${buildout:shared-part-list}. | |||
* `software_root` -- directory to install software, default to "soft" in `base_directory` | |||
* `instance_root` -- directory to create instances, default to "inst" in `base_directory` | |||
* `shared_part_root` -- directory to hold shared parts software, default to "shared" in `base_directory`. | |||
Error cases: | |||
* `PathTooDeepError` when `base_directory` is too deep. Because of limitation | |||
with the length of paths of UNIX sockets, too deep paths cannot be used. | |||
Note that once slapns work is integrated, this should not be an issue anymore. | |||
""" | |||
self._logger = logging.getLogger(__name__) | |||
# slapos proxy address | |||
self._server_ip = server_ip | |||
self._server_port = server_port | |||
self._master_url = "http://{server_ip}:{server_port}".format(**locals()) | |||
self._base_directory = base_directory | |||
self._shared_part_list = list(shared_part_list) | |||
self._slapos_commands = { | |||
'slapos-node-software': { | |||
'command': | |||
'slapos node software --cfg {self._slapos_config} --all {debug_args}', | |||
'debug_args': | |||
'--buildout-debug', | |||
'stdout_logfile': | |||
'{self._log_directory}/slapos-node-software.log', | |||
}, | |||
'slapos-node-instance': { | |||
'command': | |||
'slapos node instance --cfg {self._slapos_config} --all {debug_args}', | |||
'debug_args': | |||
'--buildout-debug', | |||
'stdout_logfile': | |||
'{self._log_directory}/slapos-node-instance.log', | |||
}, | |||
'slapos-node-report': { | |||
'command': | |||
'slapos node report --cfg {self._slapos_config} {debug_args}', | |||
'log_file': | |||
'{self._log_directory}/slapos-node-report.log', | |||
} | |||
} | |||
self._computer_id = computer_id | |||
self._slap = slap() | |||
self._slap.initializeConnection(self._master_url) | |||
self._initBaseDirectory(software_root, instance_root, shared_part_root) | |||
def _initBaseDirectory(self, software_root, instance_root, shared_part_root): | |||
"""Create the directory after checking it's not too deep. | |||
""" | |||
base_directory = self._base_directory | |||
# To prevent error: Cannot open an HTTP server: socket.error reported | |||
# AF_UNIX path too long This `base_directory` should not be too deep. | |||
# Socket path is 108 char max on linux | |||
# https://github.com/torvalds/linux/blob/3848ec5/net/unix/af_unix.c#L234-L238 | |||
# Supervisord socket name contains the pid number, which is why we add | |||
# .xxxxxxx in this check. | |||
if len(os.path.join(base_directory, 'supervisord.socket.xxxxxxx')) > 108: | |||
raise PathTooDeepError( | |||
'working directory ( {base_directory} ) is too deep'.format( | |||
**locals())) | |||
def ensureDirectoryExists(d): | |||
if not os.path.exists(d): | |||
os.mkdir(d) | |||
self._software_root = software_root if software_root else os.path.join( | |||
base_directory, 'soft') | |||
self._instance_root = instance_root if instance_root else os.path.join( | |||
base_directory, 'inst') | |||
self._shared_part_root = shared_part_root if shared_part_root else os.path.join( | |||
base_directory, 'shared') | |||
for d in (self._software_root, self._instance_root, self._shared_part_root): | |||
ensureDirectoryExists(d) | |||
os.chmod(d, 0o750) | |||
etc_directory = os.path.join(base_directory, 'etc') | |||
ensureDirectoryExists(etc_directory) | |||
self._supervisor_config = os.path.join(etc_directory, 'supervisord.conf') | |||
self._slapos_config = os.path.join(etc_directory, 'slapos.cfg') | |||
var_directory = os.path.join(base_directory, 'var') | |||
ensureDirectoryExists(var_directory) | |||
self._proxy_database = os.path.join(var_directory, 'proxy.db') | |||
# for convenience, make a slapos command for this instance | |||
bin_directory = os.path.join(base_directory, 'bin') | |||
ensureDirectoryExists(bin_directory) | |||
self._slapos_bin = os.path.join(bin_directory, 'slapos') | |||
self._log_directory = os.path.join(var_directory, 'log') | |||
ensureDirectoryExists(self._log_directory) | |||
self._supervisor_log = os.path.join(self._log_directory, 'supervisord.log') | |||
run_directory = os.path.join(var_directory, 'run') | |||
ensureDirectoryExists(run_directory) | |||
self._supervisor_pid = os.path.join(run_directory, 'supervisord.pid') | |||
self._software_pid = os.path.join(run_directory, 'slapos-node-software.pid') | |||
self._instance_pid = os.path.join(run_directory, 'slapos-node-instance.pid') | |||
self._report_pid = os.path.join(run_directory, 'slapos-node-report.pid') | |||
self._supervisor_socket = os.path.join(run_directory, 'supervisord.sock') | |||
|
|||
SupervisorConfigWriter(self).writeConfig(self._supervisor_config) | |||
SlapOSConfigWriter(self).writeConfig(self._slapos_config) | |||
SlapOSCommandWriter(self).writeConfig(self._slapos_bin) | |||
self.start() | |||
@property | |||
def computer(self): | |||
"""Access the computer. | |||
""" | |||
return self._slap.registerComputer(self._computer_id) | |||
@property | |||
def software_directory(self): | |||
# type: () -> str | |||
"""Path to software directory | |||
""" | |||
return self._software_root | |||
@property | |||
def shared_directory(self): | |||
# type: () -> str | |||
"""Path to shared parts directory | |||
""" | |||
return self._shared_part_root | |||
@property | |||
def instance_directory(self): | |||
# type: () -> str | |||
"""Path to instance directory | |||
""" | |||
return self._instance_root | |||
@property | |||
def system_supervisor_rpc(self): | |||
"""A xmlrpc connection to control the "System" supervisor. | |||
The system supervisor is used internally by StandaloneSlapOS to start | |||
slap proxy and run slapos node commands. | |||
This should be used as a context manager. | |||
""" | |||
return getSupervisorRPC(self._supervisor_socket) | |||
@property | |||
def instance_supervisor_rpc(self): | |||
"""A xmlrpc connection to control the "Instance" supervisor. | |||
The instance supervisor is the one started implictly by slapos node instance. | |||
This should be used as a context manager. | |||
""" | |||
return getSupervisorRPC( | |||
# this socket path is not configurable. | |||
os.path.join(self._instance_root, "supervisord.socket")) | |||
def format( | |||
self, | |||
partition_count, | |||
ipv4_address, | |||
ipv6_address, | |||
partition_base_name="slappart"): | |||
"""Creates `partition_count` partitions. | |||
All partitions have the same `ipv4_address` and `ipv6_address` and | |||
use the current system user. | |||
When calling this a second time with a lower `partition_count` or with | |||
different `partition_base_name` will delete existing partitions. | |||
Error cases: | |||
* ValueError when re-formatting should delete partitions that are busy. | |||
""" | |||
for path in ( | |||
self._software_root, | |||
self._shared_part_root, | |||
self._instance_root, | |||
): | |||
if not os.path.exists(path): | |||
os.mkdir(path) | |||
# check for partitions to remove | |||
unknown_partition_set = set([]) | |||
for path in glob.glob(os.path.join(self._instance_root, '*')): | |||
# var and etc are some slapos "system" directories, not partitions | |||
if os.path.isdir(path) and os.path.basename(path) not in ('var', 'etc'): | |||
unknown_partition_set.add(path) | |||
# create partitions and configure computer | |||
partition_list = [] | |||
for i in range(partition_count): | |||
partition_reference = '%s%s' % (partition_base_name, i) | |||
partition_path = os.path.join(self._instance_root, partition_reference) | |||
unknown_partition_set.discard(partition_path) | |||
if not (os.path.exists(partition_path)): | |||
os.mkdir(partition_path) | |||
os.chmod(partition_path, 0o750) | |||
partition_list.append({ | |||
'address_list': [ | |||
{ | |||
'addr': ipv4_address, | |||
'netmask': '255.255.255.255' | |||
}, | |||
{ | |||
'addr': ipv6_address, | |||
'netmask': 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' | |||
}, | |||
], | |||
'path': partition_path, | |||
'reference': partition_reference, | |||
'tap': { | |||
'name': partition_reference | |||
}, | |||
}) | |||
if unknown_partition_set: | |||
# sanity check that we are not removing partitions in use | |||
computer_partition_dict = { | |||
computer_part.getId(): computer_part | |||
for computer_part in self.computer.getComputerPartitionList() | |||
} | |||
for part in unknown_partition_set: | |||
# used in format(**locals()) below | |||
part_id = os.path.basename(part) # pylint: disable=unused-variable | |||
computer_partition = computer_partition_dict.get(os.path.basename(part)) | |||
if computer_partition is not None \ | |||
and computer_partition.getState() != "destroyed": | |||
raise ValueError( | |||
"Cannot reformat to remove busy partition at {part_id}".format( | |||
**locals())) | |||
self.computer.updateConfiguration( | |||
xml_marshaller.xml_marshaller.dumps({ | |||
'address': ipv4_address, | |||
'netmask': '255.255.255.255', | |||
'partition_list': partition_list, | |||
'reference': self._computer_id, | |||
'instance_root': self._instance_root, | |||
'software_root': self._software_root | |||
})) | |||
for part in unknown_partition_set: | |||
self._logger.debug( | |||
"removing partition no longer part of format spec %s", part) | |||
shutil.rmtree(part) | |||
def supply(self, software_url, computer_guid=None, state="available"): | |||
"""Supply a software, see ISupply.supply | |||
Software can only be supplied on this embedded computer. | |||
""" | |||
if computer_guid not in (None, self._computer_id): | |||
raise ValueError("Can only supply on embedded computer") | |||
self._slap.registerSupply().supply( | |||
software_url, | |||
self._computer_id, | |||
state=state, | |||
) | |||
def request( | |||
self, | |||
software_release, | |||
partition_reference, | |||
software_type=None, | |||
shared=False, | |||
partition_parameter_kw=None, | |||
filter_kw=None, | |||
state=None): | |||
"""Request an instance, see IRequester.request | |||
Instance can only be requested on this embedded computer. | |||
""" | |||
if filter_kw is not None: | |||
raise ValueError("Can only request on embedded computer") | |||
return self._slap.registerOpenOrder().request( | |||
software_release, | |||
software_type=software_type, | |||
partition_reference=partition_reference, | |||
shared=shared, | |||
partition_parameter_kw=partition_parameter_kw, | |||
filter_kw=filter_kw, | |||
state=state) | |||
def start(self): | |||
"""Start the system. | |||
If system was stopped, it will start partitions. | |||
If system was already running, this does not restart partitions. | |||
""" | |||
self._logger.debug("Starting StandaloneSlapOS in %s", self._base_directory) | |||
self._ensureSupervisordStarted() | |||
self._ensureSlapOSAvailable() | |||
def stop(self): | |||
"""Stops all services. | |||
This methods blocks until services are stopped or a timeout is reached. | |||
Error cases: | |||
* `Exception` when unexpected error occurs trying to stop supervisors. | |||
""" | |||
self._logger.info("shutting down") | |||
# shutdown slapos node instance supervisor, if it has been created. | |||
instance_process_alive = [] | |||
if os.path.exists(os.path.join(self._instance_root, 'etc', | |||
'supervisord.conf')): | |||
try: | |||
with self.instance_supervisor_rpc as instance_supervisor: | |||
instance_supervisor_process = psutil.Process( | |||
instance_supervisor.getPID()) | |||
instance_supervisor.stopAllProcesses() | |||
instance_supervisor.shutdown() | |||
# shutdown returns before process is completly stopped, | |||
# so wait for process. | |||
_, instance_process_alive = psutil.wait_procs( | |||
[instance_supervisor_process], timeout=10) | |||
except BaseException as e: | |||
self._logger.info("Ignoring exception while stopping instances: %s", e) | |||
with self.system_supervisor_rpc as system_supervisor: | |||
system_supervisor_process = psutil.Process(system_supervisor.getPID()) | |||
system_supervisor.stopAllProcesses() | |||
system_supervisor.shutdown() | |||
_, alive = psutil.wait_procs([system_supervisor_process], timeout=10) | |||
if alive + instance_process_alive: | |||
raise RuntimeError( | |||
"Could not terminate some processes: {}".format( | |||
alive + instance_process_alive)) | |||
def waitForSoftware(self, max_retry=0, debug=False, error_lines=30): | |||
"""Synchronously install or uninstall all softwares previously supplied/removed. | |||
This method retries on errors. If after `max_retry` times there's | |||
still an error, the error is raised, containing `error_lines` of output | |||
from the buildout command. | |||
If `debug` is true, buildout is executed in the foreground, with flags to | |||
drop in a debugger session if error occurs. | |||
Error cases: | |||
* `SlapOSNodeCommandError` when buildout error while installing software. | |||
* Unexpected `Exception` if unable to connect to embedded slap server. | |||
""" | |||
return self._runSlapOSCommand( | |||
'slapos-node-software', | |||
max_retry=max_retry, | |||
debug=debug, | |||
error_lines=error_lines, | |||
) | |||
def waitForInstance(self, max_retry=0, debug=False, error_lines=500): | |||
"""Instantiate all partitions previously requested for start. | |||
This method retries on errors. If after `max_retry` times there's | |||
still an error, the error is raised, containing `error_lines` of output | |||
from the buildout command. | |||
With instance with multiple partition, the failing partition is not | |||
always the last processed one, so by default we include more lines of | |||
output. | |||
If `debug` is true, buildout is executed in the foreground, with flags to | |||
drop in a debugger session if error occurs. | |||
Error cases: | |||
* `SlapOSNodeCommandError` when buildout error while creating instances. | |||
* Unexpected `Exception` if unable to connect to embedded slap server. | |||
""" | |||
return self._runSlapOSCommand( | |||
'slapos-node-instance', | |||
max_retry=max_retry, | |||
debug=debug, | |||
error_lines=error_lines, | |||
) | |||
def waitForReport(self, max_retry=0, debug=False, error_lines=30): | |||
"""Destroy all partitions previously requested for destruction. | |||
This method retries on errors. If after `max_retry` times there's | |||
still an error, the error is raised, containing `error_lines` of output | |||
from the buildout command. | |||
If `debug` is true, buildout is executed in the foreground, with flags to | |||
drop in a debugger session if error occurs. | |||
Error cases: | |||
* `SlapOSNodeCommandError` when buildout error while destroying instances. | |||
* Unexpected `Exception` if unable to connect to embedded slap server. | |||
""" | |||
return self._runSlapOSCommand( | |||
'slapos-node-report', | |||
max_retry=max_retry, | |||
debug=debug, | |||
error_lines=error_lines, | |||
) | |||
def _runSlapOSCommand( | |||
self, command, max_retry=0, debug=False, error_lines=30): | |||
if debug: | |||
prog = self._slapos_commands[command] | |||
# used in format(**locals()) below | |||
debug_args = prog.get('debug_args', '') # pylint: disable=unused-variable | |||
return subprocess.check_call( | |||
prog['command'].format(**locals()), shell=True) | |||
with self.system_supervisor_rpc as supervisor: | |||
retry = 0 | |||
while True: | |||
self._logger.info("starting command %s (retry:%s)", command, retry) | |||
supervisor.startProcess(command, False) | |||
delay = 0.1 | |||
while True: | |||
self._logger.debug("retry %s: sleeping %s", retry, delay) | |||
# we start waiting a short delay and increase the delay each loop, | |||
# because when software is already built, this should return fast, | |||
# but when we build a full software we don't need to poll the | |||
# supervisor too often. | |||
time.sleep(delay) | |||
delay = min(delay * 1.2, 300) | |||
process_info = supervisor.getProcessInfo(command) | |||
if process_info['statename'] in ('EXITED', 'FATAL'): | |||
self._logger.debug("SlapOS command finished %s" % process_info) | |||
if process_info['exitstatus'] == 0: | |||
return | |||
if retry >= max_retry: | |||
# get the last lines of output, at most `error_lines`. If | |||
# these lines are long, the output may be truncated. | |||
_, log_offset, _ = supervisor.tailProcessStdoutLog(command, 0, 0) | |||
output, _, _ = supervisor.tailProcessStdoutLog( | |||
command, log_offset - (2 << 13), 2 << 13) | |||
raise SlapOSNodeCommandError({ | |||
'output': '\n'.join(output.splitlines()[-error_lines:]), | |||
'exitstatus': process_info['exitstatus'], | |||
}) | |||
break | |||
retry += 1 | |||
def _ensureSupervisordStarted(self): | |||
if os.path.exists(self._supervisor_pid): | |||
with open(self._supervisor_pid, 'r') as f: | |||
try: | |||
pid = int(f.read()) | |||
except (ValueError, TypeError): | |||
self._logger.debug( | |||
"Error reading supervisor pid from file, assuming it's not running" | |||
) | |||
else: | |||
try: | |||
process = psutil.Process(pid) | |||
except psutil.NoSuchProcess: | |||
pass | |||
else: | |||
if process.name() == 'supervisord': | |||
# OK looks already running | |||
self._logger.debug("Supervisor running with pid %s", pid) | |||
return | |||
self._logger.debug("Supervisor pid file seem stale") | |||
# start new supervisord | |||
output = subprocess.check_output( | |||
['supervisord'], | |||
cwd=self._base_directory, | |||
) | |||
self._logger.debug("Started new supervisor: %s", output) | |||
def _isSlapOSAvailable(self): | |||
try: | |||
urllib.request.urlopen(self._master_url).close() | |||
except urllib.error.HTTPError as e: | |||
# root URL (/) of slapproxy is 404 | |||
if e.code == http_client.NOT_FOUND: | |||
return True | |||
raise | |||
except urllib.error.URLError as e: | |||
if e.reason.errno == errno.ECONNREFUSED: | |||
return False | |||
raise | |||
except socket.error as e: | |||
if e.errno == errno.ECONNRESET: | |||
return False | |||
raise | |||
except http_client.HTTPException: | |||
return False | |||
return True # (if / becomes 200 OK) | |||
def _ensureSlapOSAvailable(self): | |||
# Wait for proxy to accept connections | |||
for i in range(2**8): | |||
if self._isSlapOSAvailable(): | |||
return | |||
time.sleep(i * .01) | |||
raise RuntimeError("SlapOS not started") |