Mini Shell
# vim: set ts=4 sw=4 expandtab syntax=python:
"""
ngxconf.builder
Configuration builder functions
Copyright (c) 2017-2020 InMotion Hosting, Inc.
http://www.inmotionhosting.com/
@author J. Hipps <jacobh@inmotionhosting.com>
"""
import os
import sys
import codecs
import re
import pwd
import json
import logging
import signal
from glob import glob
import yaml
from yaml import CDumper, CLoader
import arrow
import pem
import netifaces
from ngxconf import __version__, __date__, default_conf, default_header
from ngxconf import fpm, cpfpm
from ngxconf.util import load_template, gconf, dict_strip_mtime, cp_get_package_data, cp_get_userinfo, get_profile
logger = logging.getLogger('ngxconf')
udefs = None
def rebuild_all(force=False, defaults=False, skip_fpm=False, userdef=None):
"""
Rebuild configuration for all users
"""
global udefs
udefs = userdef
ngx_changes = []
fpm_changes = []
# Register signal handler & check for/register lock
signal.signal(signal.SIGINT, sigint_handler)
signal.signal(signal.SIGUSR1, sigusr_handler)
check_lock(kill_previous=True)
# Build default/catch-all server block, if enabled
if gconf.enable_catchall:
build_default()
# Enumerate cPanel users
try:
ulist = os.listdir("/var/cpanel/users")
logger.debug("Enumerated %d users", len(ulist))
except Exception as e:
logger.error("Failed to enumerate users from /var/cpanel/users: %s", str(e))
return None
# Load template
t_server = load_template('user_server')
# Rebuild config for each user
for tuser in ulist:
# Skip invalid users
if tuser in ['system']:
logger.debug("Skipping invalid user '%s'", tuser)
continue
nchg = rebuild_user(tuser, force=force, template=t_server, defaults=defaults, skip_fpm=skip_fpm)
if nchg:
ngx_changes += nchg['nginx']
fpm_changes += nchg['fpm']
# Purge config files for users who no longer exist
clist = set()
try:
for tfile in os.listdir("/etc/nginx/vhosts"):
clist.add(tfile.split('__')[0])
except Exception as e:
logger.error("Failed to enumerate vhost includes: %s", str(e))
nlist = clist - set(ulist)
for tuser in nlist:
try:
tglob = glob("/etc/nginx/vhosts/%s__*.conf" % (tuser))
except Exception as e:
logger.error("Failed to enumerate vhost includes for %s: %s", tuser, str(e))
continue
for tfile in tglob:
try:
os.unlink(tfile)
logger.debug("Removed stale file: %s", tfile)
fpm_changes.append(tfile.split('__', 1)[1].replace('_', '.'))
except Exception as e:
logger.error("Failed to remove stale Nginx include %s: %s", tfile, str(e))
continue
# Finish up: clear lock & return number of changes
logger.info("Rebuilt configuration for %d users -- CHANGED: Nginx: %d / FPM: %d",
len(ulist), len(ngx_changes), len(fpm_changes))
clear_lock()
return {'nginx': ngx_changes, 'fpm': fpm_changes}
def rebuild_user(user, force=False, outfile=None, template=None, defaults=False, skip_fpm=False, userdef=None):
"""
Rebuild configuration for single user
If @defaults is True, reverts all user configs to defaults
"""
global udefs
if userdef:
udefs = userdef
# Parse cPanel userdata
udata = parse_userdata(user)
if udata is None:
logger.error("Rebuild of user '%s' failed", user)
return None
# Parse user info file
uinfo = cp_get_userinfo(user)
if uinfo is None:
logger.error("update_user: aborting due to invalid user info")
return None
# Parse user configuration (creating default configs if the don't exist)
uconf = parse_userconf(user, udata, uinfo, defaults=defaults, force=force)
# Update php-fpm configuration
if gconf.enable_fpm and not skip_fpm:
if gconf.fpm_management.lower() == 'ngxconf':
fpm_updates = fpm.update_user(user, udata, uconf, uinfo, force=force)
elif gconf.fpm_management.lower() == 'cpanel':
vlist = [x for x in udata.keys() if not x.startswith('_') and not uconf.get(x, {}).get('_exclude', False)]
fpm_updates = cpfpm.update_fpm(user, vlist, force=force)
else:
logger.error("FPM CONFIGURATION NOT BUILT: Unrecognized `fpm_system` selection '%s'", gconf.fpm_system)
fpm_updates = []
if len(fpm_updates) > 0:
logger.info("PHP-FPM configuration updated for %s [%s]", user, ', '.join(fpm_updates))
else:
fpm_updates = []
# Get mtimes of last nginx conf builds
utime = get_last_nconf_updates(user)
# Load template
if template is None:
t_server = load_template('user_server')
else:
t_server = template
# Ensure user cache zones exist
try:
cache_path = '%s/%s' % (gconf.cache_base_path, user)
os.stat(cache_path)
except:
try:
os.mkdir(cache_path)
except Exception as e:
logger.error("Unable to create user cache zone directory: %s", str(e))
# Ensure user cache zone perms/ownership
try:
ngx_pwd = pwd.getpwnam(gconf.nginx_user)
os.chmod(cache_path, 0o0775)
os.chown(cache_path, ngx_pwd.pw_uid, ngx_pwd.pw_gid)
except Exception as e:
logger.error("Unable to set user cache zone perms/ownership: %s", str(e))
# Build Nginx config for each domain, if it has changed (or force=True)
# tconf = user config, tdat = cpanel userdata, tdom = domain
ngx_updates = []
for tdom, tdat in udata.items():
if tdom.startswith('_'):
continue
tconf = uconf.get(tdom)
if tconf:
# Check for user-defined domain includes
uinclude = None
if gconf.allow_user_includes:
try:
incpath = '/home/%s/.imh/nginx/%s.inc.conf' % (user, tdom.replace('.', '_'))
if os.path.exists(incpath):
with open(incpath, 'r') as f:
uinclude = f.read()
logger.debug("Read %d lines from user include %s", len(uinclude.splitlines()), incpath)
else:
logger.debug("No user include found at %s", incpath)
except Exception as e:
logger.error("Failed to read user include file at [%s]: %s", incpath, str(e))
# Check to see if the userdata _SSL file was changed
try:
ssl_uconf_changed = utime.get(tdom, 0) < udata[tdom]['_ssl']['_mtime']
except:
ssl_uconf_changed = False
try:
if tconf.get('_exclude') is True:
logger.debug("Excluding %s:%s", user, tdom)
try:
outconf = "/etc/nginx/vhosts/%s__%s.conf" % (user, tdom.replace('.', '_'))
os.unlink(outconf)
except:
pass
elif force or (utime.get(tdom, 0) < udata[tdom]['_mtime']) \
or (utime.get(tdom, 0) < uconf[tdom]['_mtime']) \
or (utime.get(tdom, 0) < int(os.stat(f"/var/cpanel/users/{user}").st_mtime)):
# render user nginx config from template
b_main = (tdom == udata['_main']['main_domain'])
ngx_updates.append(tdom)
if outfile:
render_config(user, tdom, tconf, tdat, t_server, is_main=b_main, basepath=outfile, userinc=uinclude)
else:
render_config(user, tdom, tconf, tdat, t_server, is_main=b_main, userinc=uinclude)
else:
logger.debug("Skipping %s:%s -- up-to-date", user, tdom)
except Exception as e:
logger.error("Render for %s:%s failed: %s", user, tdom, str(e))
else:
logger.warning("Skipping rebuild for %s:%s due to invalid data", user, tdom)
# Remove config files for any domains that have been removed
for tdom in utime.keys():
if not udata.get(tdom):
tpath = "/etc/nginx/vhosts/%s__%s.conf" % (user, tdom.replace('.', '_'))
try:
os.unlink(tpath)
logger.debug("Removed stale file: %s", tpath)
fpm_updates.append(tdom)
except Exception as e:
logger.error("Unable to remove stale config %s: %s", tpath, str(e))
return {'nginx': ngx_updates, 'fpm': fpm_updates}
def build_default(outconf='/etc/nginx/conf.d/default.conf', use_ipv6=False):
"""
Build default.conf from template
"""
template = load_template('default_server')
# get list of IPs
iplist = []
ilraw = map(lambda x: map(lambda y: y.get('addr'),
netifaces.ifaddresses(x).get(netifaces.AF_INET, [])),
netifaces.interfaces())
if use_ipv6:
ilraw += map(lambda x: map(lambda y: y.get('addr'),
netifaces.ifaddresses(x).get(netifaces.AF_INET6, [])),
netifaces.interfaces())
for tip in ilraw:
iplist += tip
# filter out unwanted IPs
iplist = list(set([x for x in iplist if not re.match(r'^(127\.0\.0\.|::1|fe80|fd00)', x)]))
logger.debug("Detected %d IPs on the server: %s", len(iplist), ', '.join(iplist))
# determine main IP
try:
with open('/var/cpanel/mainip', 'r') as f:
main_ip = f.read().strip()
except Exception as e:
logger.error("Failed to read main IP from /var/cpanel/mainip: %s", str(e))
main_ip = iplist[0]
logger.debug("Got main IP: %s", main_ip)
# write system service ssl pem/key
dconf = {'ssl_certificate': None, 'ssl_key': None}
cpath = '/var/cpanel/ssl/cpanel/mycpanel.pem'
dconf['ssl_certificate'], dconf['ssl_key'] = create_cert_from_combined('mycpanel', cpath)
# render with Jinja2
try:
ttext = template.render(tstamp=arrow.now().format(), ips=iplist, dconf=dconf,
tls_cipher_list=gconf.tls_cipher_list, main_ip=main_ip,
ngxconf_ver="%s (%s)" % (__version__, __date__))
except Exception as e:
logger.error("Failed to render default config: %s", str(e))
return False
# write new file
try:
with open(outconf, 'w') as f:
f.write(ttext)
logger.debug("Rendered default config to %s", outconf)
except Exception as e:
logger.error("Failed to render default config to file %s: %s", outconf, str(e))
return False
return True
def render_config(user, userdom, userconf, userdata, template, is_main=False, basepath='/etc/nginx/vhosts', userinc=None):
"""
Compile template using Jinja2
"""
outconf = "%s/%s__%s.conf" % (basepath, user, userdom.replace('.', '_'))
# create list of cPanel domains
cpdomains = {'cpanel': [], 'webmail': []}
try:
for talias in userdata['serveralias'].split():
if not talias.startswith("www.") and not talias.startswith("mail."):
cpdomains['cpanel'].append("cpanel." + talias)
cpdomains['webmail'].append("webmail." + talias)
except Exception as e:
logger.error("Failed to create list of cPanel subdomains for %s: %s", userdom, str(e))
cpdomains = {'cpanel': None, 'webmail': None}
# render with Jinja2
try:
ttext = template.render(tstamp=arrow.now().format(), domain=userdom, cache_name=user, is_main=is_main,
cache_key_format=gconf.cache_key_format, user=user, uconf=userconf,
http_ip=gconf.http_listen_interface, cp_domains=cpdomains,
rl_whitelist=gconf.use_ratelimit_whitelist, restrict_purge=gconf.restrict_purge,
tls_cipher_list=gconf.tls_cipher_list, udata=userdata,
user_include=userinc, cpanel_host_redirect=gconf.cpanel_host_redirect,
manage_tls_options=gconf.manage_tls_options,
ngxconf_ver="%s (%s)" % (__version__, __date__))
except Exception as e:
logger.error("Failed to render template for %s:%s: %s", user, userdom, str(e))
return False
# write new include file
try:
with codecs.open(outconf, 'w', 'utf-8', errors='ignore') as f:
f.write(ttext)
logger.debug("Rendered config for %s:%s to %s", user, userdom, outconf)
except Exception as e:
logger.error("Failed to render template to file %s: %s", outconf, str(e))
return False
return True
def parse_userconf(user, udata, uinfo, defaults=False, force=False):
"""
Parse Nginx user config files
Default configuration will be created if missing
"""
global udefs
idir = '/home/%s/.imh' % (user)
udir = '/home/%s/.imh/nginx' % (user)
# Check for config dir
try:
os.stat(udir)
except:
try:
os.makedirs(udir, mode=0o0775)
logger.debug("Created directory %s", udir)
except Exception as e:
logger.error("Unable to create directory %s: %s", udir, str(e))
return False
if udata is None:
logger.error("No userdata associated with '%s' account; skipping", udata)
return None
# Get user UID/GID
try:
u_pwd = pwd.getpwnam(user)
except Exception as e:
logger.error("Failed to translate username '%s' to UID/GID: %s", user, str(e))
return None
# Ensure ~/.imh dir perms/ownership
try:
os.chmod(idir, 0o0775)
os.chown(idir, u_pwd.pw_uid, u_pwd.pw_gid)
except Exception as e:
logger.warning("Unable to set permissions/ownership of %s: %s", udir, str(e))
# Ensure nginx dir perms/ownership
try:
os.chmod(udir, 0o0775)
os.chown(udir, u_pwd.pw_uid, u_pwd.pw_gid)
except Exception as e:
logger.warning("Unable to set permissions/ownership of %s: %s", udir, str(e))
package_data = cp_get_package_data(uinfo.get('PLAN'))
tprofile = package_data.get('IMH_NGX_PROFILE')
ucdata = {}
for tdom, tdat in udata.items():
if tdom.startswith('_'):
continue
tfile = '%s/%s.yml' % (udir, tdom.replace('.', '_'))
# Check if file exists; if not, create with default contents
try:
ustat = os.stat(tfile)
except Exception:
ustat = None
# Also revert back to defaults if @defaults is True
if ustat is None or defaults:
logger.debug("Copying default userconf to %s", tfile)
create_default_userconf(user, tfile, tdat, profile=tprofile)
try:
ustat = os.stat(tfile)
except Exception as e:
logger.error("Userconf not found (creation failed) %s: %s", tfile, str(e))
continue
try:
with open(tfile, 'r') as f:
ucdata[tdom] = {}
ucdata[tdom].update(default_conf)
ucdata[tdom].update(yaml.load(f, Loader=CLoader))
ucdata[tdom]['_mtime'] = int(ustat.st_mtime)
except Exception as e:
logger.error("Failed to load userconf for %s: %s", tdom, str(e))
logger.warning("Reverting user config to defaults due to parsing errors: %s", tfile)
create_default_userconf(user, tfile, tdat, profile=tprofile)
try:
with open(tfile, 'r') as f:
ucdata[tdom] = yaml.load(f, Loader=CLoader)
ucdata[tdom]['_mtime'] = int(os.stat(tfile).st_mtime)
except Exception as e:
ucdata[tdom] = None
logger.error("Default userconf creation failed %s: %s", tfile, str(e))
continue
# Check if user_default_override_local is enabled
override_update = False
if gconf.user_default_override_local and udefs:
preup = json.dumps(ucdata[tdom])
ucdata[tdom].update(udefs)
postup = json.dumps(ucdata[tdom])
if postup != preup:
logger.info("Applied user overrides to %s; domain config updated", tdom)
override_update = True
else:
logger.debug("User overrides already applied to %s; no change detected", tdom)
# Set override to disable nginx cache
if package_data.get('IMH_DISABLE_CACHE') == "1":
if ucdata[tdom]['pass_all'] != True:
logger.info("Applied 'disable cache' user overrides to %s; domain config updated", tdom)
ucdata[tdom]['pass_all'] = True
override_update = True
else:
logger.debug("User overrides 'disable cache' already applied to %s; no change detected", tdom)
# Check if SSL options are up-to-date
ssl_update = False
if tdat.get('_ssl'):
try:
certpath = "%s/%s.pem" % (gconf.chained_cert_path, tdom.replace('-', '--').replace('.', '-'))
mtime_cert = int(os.stat(certpath).st_mtime)
except:
mtime_cert = 0
if force or tdat['_ssl']['_mtime'] > ucdata[tdom]['_mtime'] or \
tdat['_ssl']['_mtime'] > mtime_cert:
# Update SSL cert
combpath = '/var/cpanel/ssl/apache_tls/%s/combined' % (tdat['servername'])
if os.path.exists(combpath):
newcert, newkey = create_cert_from_combined(tdat['servername'], combpath)
else:
newcert = create_chained_cert(tdat['servername'],
tdat['_ssl'].get('sslcertificatefile'),
tdat['_ssl'].get('sslcacertificatefile'))
newkey = tdat['_ssl'].get('sslcertificatekeyfile')
if newcert is not None and newkey is not None:
ucdata[tdom]['ssl_enabled'] = True
ucdata[tdom]['ssl_certificate'] = newcert
ucdata[tdom]['ssl_certificate_key'] = newkey
ucdata[tdom]['proxy_proto'] = "https"
ssl_update = True
else:
ucdata[tdom]['ssl_enabled'] = False
ucdata[tdom]['proxy_proto'] = "http"
ucdata[tdom]['ssl_certificate'] = None
ucdata[tdom]['ssl_certificate_key'] = None
logger.warning("Invalid SSL vhost defined for %s", user)
else:
if ucdata[tdom].get('ssl_enabled'):
# Remove old chained cert
try:
os.unlink(ucdata[tdom]['ssl_certificate'])
logger.info("Removed unused chained certificate: %s", ucdata[tdom]['ssl_certificate'])
except Exception as e:
logger.warning("Failed to removed chained certificate %s: %s",
ucdata[tdom]['ssl_certificate'], str(e))
ucdata[tdom]['ssl_enabled'] = False
ucdata[tdom]['proxy_proto'] = "http"
ucdata[tdom]['ssl_certificate'] = None
ucdata[tdom]['ssl_certificate_key'] = None
ssl_update = True
# Write updated userconf YAML file if SSL or override params have changed
if ssl_update or override_update:
if ssl_update:
logger.info("Updated ssl_enabled status for %s -> %s", tdom, ucdata[tdom]['ssl_enabled'])
try:
with open(tfile, 'w') as f:
f.write(default_header.strip() + '\n')
yaml.dump(dict_strip_mtime(ucdata[tdom]), stream=f, Dumper=CDumper, default_flow_style=False)
logger.debug("Updated SSL and/or override parameters in %s", tfile)
except Exception as e:
logger.error("Unable to write updated YAML file: %s", str(e))
# Ensure correct file permissions
try:
os.chmod(tfile, 0o0664)
os.chown(tfile, u_pwd.pw_uid, u_pwd.pw_gid)
except Exception as e:
logger.warning("Unable to set permissions/ownership of %s: %s", tfile, str(e))
# Grab new mtime so that Nginx conf will be updated during the template rendering
try:
udata[tdom]['_mtime'] = int(os.stat(tfile).st_mtime)
except Exception as e:
logger.error("Failed to stat updated userconf %s: %s", tfile, str(e))
logger.debug("Parsed userconf for %s", user)
return ucdata
def create_default_userconf(user, ufile, udata, profile=None):
"""
Create YAML file containing default user config
"""
global udefs
# Setup default base config based on chosen profile
profile_name = profile
cdata = None
if profile is not None:
cdata = get_profile(profile)
if cdata is None:
cdata = {}
cdata.update(default_conf)
profile_name = "(builtin_default)"
if udefs:
cdata.update(udefs)
if udata.get('_ssl'):
combpath = '/var/cpanel/ssl/apache_tls/%s/combined' % (udata['servername'])
if os.path.exists(combpath):
newcert, newkey = create_cert_from_combined(udata['servername'], combpath)
else:
newcert = create_chained_cert(udata['servername'],
udata['_ssl'].get('sslcertificatefile'),
udata['_ssl'].get('sslcacertificatefile'))
newkey = udata['_ssl'].get('sslcertificatekeyfile')
if newcert is not None and newkey is not None:
cdata['ssl_enabled'] = True
cdata['ssl_certificate'] = newcert
cdata['ssl_certificate_key'] = newkey
cdata['proxy_proto'] = "https"
else:
logger.warning("Invalid SSL vhost defined for %s", user)
# Write userconf YAML file
try:
with open(ufile, 'w') as f:
f.write(default_header.strip() + '\n')
yaml.dump(dict_strip_mtime(cdata), stream=f, Dumper=CDumper, default_flow_style=False)
except Exception as e:
# On VZ containers root can't create, chown, or write to a user:user file if quota is exceeded for user
# To work around this, remove the user:user file and recreate it as root:root (which ngxconf can read/write)
if "Disk quota exceeded" in str(e):
logger.error('Disk quota exceeded. Unable to write to %s', ufile)
os.unlink(ufile)
with open(ufile, 'w') as f:
f.write(default_header.strip() + '\n')
yaml.dump(dict_strip_mtime(cdata), stream=f, Dumper=CDumper, default_flow_style=False)
logger.debug("Wrote default configuration for %s from profile %s as root:root", user, profile_name)
return True
logger.error("Unable to write default userconf %s: %s", ufile, str(e))
return False
# Get user UID/GID
try:
u_pwd = pwd.getpwnam(user)
except Exception as e:
logger.error("Failed to translate username '%s' to UID/GID: %s", user, str(e))
return None
# Ensure correct file permissions
try:
os.chmod(ufile, 0o0664)
os.chown(ufile, u_pwd.pw_uid, u_pwd.pw_gid)
except Exception as e:
logger.warning("Unable to set permissions/ownership of %s: %s", ufile, str(e))
logger.debug("Wrote default configuration for %s from profile %s", user, profile_name)
return True
def create_cert_from_combined(domain, cpath):
"""
Create a chained certificate from cPanel combined TLS data
"""
chained = "%s/%s.pem" % (gconf.chained_cert_path, domain.replace('-', '--').replace('.', '-'))
keypath = "%s/%s.key" % (gconf.chained_cert_path, domain.replace('-', '--').replace('.', '-'))
ckey = ""
certdata = ""
# Parse combined PEM certificate
try:
clist = pem.parse_file(cpath)
except Exception as e:
logger.error("Failed to parse combined cert for %s at %s: %s", domain, cpath, str(e))
return (None, None)
for tpem in clist:
if isinstance(tpem, pem.Key):
ckey = str(tpem)
elif isinstance(tpem, pem.Certificate):
certdata += str(tpem)
else:
logger.warning("Found unexpected PEM component of type <%s> in %s", type(tpem), cpath)
if not len(ckey) or not len(certdata):
logger.error("Missing certificate and/or keys from combined TLS store at %s", cpath)
return (None, None)
# Write chained cert
try:
with open(chained, 'w') as f:
f.write(certdata)
logger.debug("Wrote chained certificate from cP 68+ combined format: %s", chained)
except Exception as e:
logger.error("Failed to write chained certificate for %s: %s", domain, str(e))
return (None, None)
# Write key
try:
with open(keypath, 'w') as f:
f.write(ckey)
os.chmod(keypath, 0o0600)
logger.debug("Wrote keyfile from cP 68+ combined format: %s", keypath)
except Exception as e:
logger.error("Failed to write keyfile for %s: %s", domain, str(e))
return (None, None)
return (chained, keypath)
def create_chained_cert(domain, certpath, capath, outfile=None):
"""
Created a chained certificate consisting of the certificate + CA bundle
"""
if outfile is None:
chained = "%s/%s.pem" % (gconf.chained_cert_path, domain.replace('-', '--').replace('.', '-'))
else:
chained = outfile
# Read certificate
try:
with open(certpath, 'r') as f:
certdata = f.read().strip()
except Exception as e:
logger.error("Failed to read certificate file for %s: %s", domain, str(e))
return None
# Read intermediates (cabundle)
if capath is not None:
try:
with open(capath, 'r') as f:
cadata = f.read().strip()
except Exception as e:
logger.error("Failed to read CA bundle for %s: %s", domain, str(e))
cadata = ""
else:
logger.warning("No CA bundle defined for %s", domain)
cadata = ""
# Write chained cert
try:
with open(chained, 'w') as f:
f.write(certdata + '\n' + cadata)
logger.debug("Wrote chained certificate: %s", chained)
except Exception as e:
logger.error("Failed to write chained certificate for %s: %s", domain, str(e))
return None
return chained
def parse_userdata(user):
"""
Parse cPanel userdata for @user
"""
main = parse_userdata_file(user, 'main')
if main is None:
logger.warning("Abort parsing userdata for user '%s'", user)
return None
domlist = [main['main_domain']] + main['sub_domains']
udata = {'_main': main, '_suspended': check_suspended(user)}
for tdom in domlist:
tdata = parse_userdata_file(user, tdom)
tssl = parse_userdata_file(user, tdom, check_ssl=True)
if tdata is None:
continue
if tssl:
try:
tdata['_ssl'] = tssl
except Exception as e:
logger.error("Failed to extract SSL information for %s:%s - %s", user, tdom, str(e))
tdata['_ssl'] = None
else:
tdata['_ssl'] = None
udata[tdom] = {**tdata, '_suspended': udata['_suspended']}
return udata
def parse_userdata_file(user, ufile, check_ssl=False, prefix='/var/cpanel/userdata'):
"""
Parse single cPanel userdata file; injects [_mtime] field from stat result
"""
datfile = "%s/%s/%s" % (prefix, user, ufile)
if check_ssl:
datfile += "_SSL"
try:
os.stat(datfile)
except:
return None
try:
with open(datfile, 'r') as f:
ddata = yaml.load(f, Loader=CLoader)
ddata['_mtime'] = int(os.stat(datfile).st_mtime)
return ddata
except Exception as e:
logger.error("Failed to parse userdata file %s: %s", datfile, str(e))
return None
def check_suspended(user):
"""
Check if @user is suspended; if so, return a normalized reason string,
otherwise, return None
"""
suspath = "/var/cpanel/suspended/%s" % (user)
try:
os.stat(suspath)
with open(suspath, 'r') as f:
rreason = f.read().strip()
except:
return None
try:
rd = re.match(r'^((?:(?P<reasonA>[a-z]+):(?P<detail>.+))|(?:\[PP2 Auto\] - Reason: (?P<reasonB>.+)))$',
rreason).groupdict()
return (rd['reasonA'] if rd['reasonA'] else rd['reasonB']).strip().lower()
except:
logger.warning("Failed to parse suspension reason from %s", suspath)
return 'unknown'
def get_last_nconf_updates(user, basedir='/etc/nginx/vhosts'):
"""
Return a dict of mtime (int) for each nginx config file
"""
nconf = {}
for tpath in glob("%s/%s__*.conf" % (basedir, user)):
# extract domain from filename
tfile = os.path.basename(tpath)
try:
tdom = tfile.replace('.conf', '').split('__', 1)[1].replace('_', '.')
except:
logger.error("Failed to parse domain from vhost config: %s", tfile)
continue
# grab the mtime
try:
nconf[tdom] = int(os.stat(tpath).st_mtime)
except Exception as e:
logger.error("Failed to stat vhost config %s: %s", tpath, str(e))
continue
return nconf
def check_lock(lockfile='/var/run/ngxconf.pid', kill_previous=False):
"""
Check if lockfile exists; if not, create one
If lockfile DOES exist and @kill_previous is True,
kills the previous ngxconf runner and create a new lockfile
"""
try:
os.stat(lockfile)
with open(lockfile, 'r') as f:
old_pid = int(f.read())
os.kill(old_pid, 0)
logger.warning("ngxconf is already running with PID %d", old_pid)
if kill_previous:
logger.warning("Halting running instance with SIGUSR1. This ngxconf will take over.")
os.kill(old_pid, signal.SIGUSR1)
else:
logger.error("Aborting due to ngxconf already running (%d)", old_pid)
sys.exit(10)
except:
pass
# Write new lock file with current PID
tpid = str(os.getpid())
with open(lockfile, 'w') as f:
f.write(tpid)
logger.debug("Wrote lock file %s (PID %s)", lockfile, tpid)
def clear_lock(lockfile='/var/run/ngxconf.pid'):
"""
Remove lock/pidfile
"""
try:
os.unlink(lockfile)
logger.debug("Lock file removed: %s", lockfile)
return True
except Exception as e:
logger.warning("Failed to remove lockfile %s: %s", lockfile, str(e))
return False
def sigusr_handler(sig, frame): # pylint: disable=unused-argument
"""
Signal handler for SIGUSR1
"""
logger.error("Received signal from another running ngxconf. Aborting.")
sys.exit(10)
def sigint_handler(sig, frame): # pylint: disable=unused-argument
"""
Signal handler for SIGINT (interrupt)
"""
logger.error("Interrupted.")
clear_lock()
sys.exit(11)
Zerion Mini Shell 1.0