Mini Shell
# vim: set ts=4 sw=4 expandtab syntax=python:
"""
ngxconf.masters
FPM Masters: Supervisord XML-RPC Interface & Config Management
Copyright (c) 2018-2020 InMotion Hosting, Inc.
http://www.inmotionhosting.com/
@author J. Hipps <jacobh@inmotionhosting.com>
"""
# pylint: disable=invalid-name
import os
import logging
from xmlrpc.client import ServerProxy
import arrow
try:
from supervisor.xmlrpc import SupervisorTransport
except ImportError:
SupervisorTransport = None
from ngxconf import __version__, __date__
from ngxconf.util import load_template, gconf
logger = logging.getLogger('ngxconf')
class Supervisor(object):
"""
Class for wrapping supervisord communications
and managing PHP-FPM masters
"""
sockpath = None
rpc = None
connected = False
template = None
confpath = "/opt/ngxconf/supervisord/conf.d"
def __init__(self, sockpath='/var/run/supervisord-fpm.sock'):
self.sockpath = sockpath
if SupervisorTransport is None:
logger.error("Machine is missing the imh-python-supervisor package")
else:
self.connect()
def connect(self):
"""
Initialize supervisord XMLRPC connection via UNIX socket
"""
try:
self.rpc = ServerProxy('http://localhost', transport=SupervisorTransport(None, None, serverurl='unix://' + self.sockpath))
except Exception as e:
logger.error("Failed to spawn XMLRPC connection to supervisord: %s", str(e))
self.connected = False
return False
# Check to ensure connection is established
try:
cstate = self.rpc.supervisor.getState()
spid = self.rpc.supervisor.getPID()
logger.debug("Connected to supervisord (pid %d) on %s [state: %s (%s)]", spid, self.sockpath, cstate['statename'], cstate['statecode'])
except Exception as e:
logger.error("Failed to connect to supervisord master at %s: %s", self.sockpath, str(e))
self.connected = False
return False
if cstate['statecode'] != 1:
logger.warning("Supervisord is in state %s (%s)", cstate['statename'], cstate['statecode'])
self.connected = True
return True
def update_config(self, reload_list=None):
"""
Reload supervisord configuration, and start/stop/restart
process groups accordingly
If @reload_list is set, it should be a list [] of vhosts
that this function will ensure get restarted
"""
if not self.connected:
logger.error("supervisord: Not connected, unable to reload config")
return -1
try:
added, changed, removed = self.rpc.supervisor.reloadConfig()[0]
logger.debug("supervisord: reloadConfig: %d added / %d changed / %d removed",
len(added), len(changed), len(removed))
except Exception as e:
self.connected = False
logger.error("supervisord: Failed to reload config: %s", str(e))
return -1
fails = 0
# ensure we have a connection
self.rpc.supervisor.getState()
changeset = set(changed)
if reload_list:
changeset.update(reload_list)
changeset = changeset.difference(added)
for tgroup in removed:
logger.info("supervisord: ProcessGroup removed: %s", tgroup)
try:
self.rpc.supervisor.stopProcessGroup(tgroup)
self.rpc.supervisor.removeProcessGroup(tgroup)
changeset.discard(tgroup)
except Exception:
# Not counted as a failure, since it should already be removed
pass
for tgroup in changeset:
logger.info("supervisord: ProcessGroup changed: %s", tgroup)
try:
self.rpc.supervisor.stopProcessGroup(tgroup)
self.rpc.supervisor.removeProcessGroup(tgroup)
self.rpc.supervisor.addProcessGroup(tgroup)
self.rpc.supervisor.startProcessGroup(tgroup, False)
except Exception as e:
logger.error("Failed to change ProcessGroup %s: %s", tgroup, str(e))
fails += 1
for tgroup in added:
logger.info("supervisord: ProcessGroup added: %s", tgroup)
try:
self.rpc.supervisor.addProcessGroup(tgroup)
self.rpc.supervisor.startProcessGroup(tgroup, False)
except Exception as e:
logger.error("Failed to add ProcessGroup %s: %s", tgroup, str(e))
fails += 1
logger.info("supervisord: Reload complete (%d failures)", fails)
return fails
def get_info(self):
"""
Return status of all ProcessGroups
"""
return self.rpc.supervisor.getAllProcessInfo()
def render_config(self, pgname, binpath, confpath):
"""
Render config file for ProcessGroup @pgname
"""
if not self.template:
self.template = load_template('user_supervisor')
outconf = os.path.join(self.confpath, pgname + '.conf')
phpconf = os.path.join(gconf.fpm_conf_path, pgname + '.conf')
# render with Jinja2
try:
ttext = self.template.render(tstamp=arrow.now().format(), poolname=pgname,
php_binpath=binpath, php_confpath=phpconf,
ngxconf_ver="%s (%s)" % (__version__, __date__))
except Exception as e:
logger.error("Failed to render supervisord template for %s: %s", pgname, str(e))
return False
# write new include file
try:
with open(outconf, 'w') as f:
f.write(ttext)
logger.debug("Rendered supervisord config for %s to %s", pgname, outconf)
except Exception as e:
logger.error("Failed to render template to file %s: %s", outconf, str(e))
return False
return True
def restart_group(self, pgname):
self.stop_group(pgname)
return self.start_group(pgname)
def stop_group(self, pgname):
try:
self.rpc.stopProccessGroup(pgname)
except Exception as e:
logger.error("Failed to stop ProcessGroup %s: %s", pgname, str(e))
return False
return True
def start_group(self, pgname):
try:
self.rpc.startProccessGroup(pgname)
except Exception as e:
logger.error("Failed to start ProcessGroup %s: %s", pgname, str(e))
return False
return True
Zerion Mini Shell 1.0