Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 266 lines (209 sloc) 8.17 KB
#!/usr/bin/env python3
"""Enforcer is a prehook and posthook for Tea4CUPS whose primary purpose is to
add and subtract from page quotas as user jobs are processed.
When a user sends a job to the print server, Tea4CUPS passes the job as a
PostScript file to enforcer before sending it to the printer. Enforcer calls
reads PostScript comments to count pages and connects to mysql to check against
the user's quota. If enforcer returns 255 (a.k.a. -1), the job is rejected.
Otherwise, enforcer gets called again when the job is done and logs the job in
mysql, taking care to set the page count to zero if an error was encountered.
Another function of enforcer is to send notifications to desktops to let users
know when a job has been sent to the printer, been rejected due to the quota,
finished printing, or failed to print.
"""
import argparse
import os
import subprocess
import sys
from collections import namedtuple
from configparser import ConfigParser
from datetime import datetime
from syslog import syslog
from textwrap import dedent
from traceback import format_exc
import ocflib.printing.quota as quota
import redis
from ocflib.misc.mail import MAIL_SIGNATURE
from ocflib.misc.mail import send_mail_user
from ocflib.misc.mail import send_problem_report
CONF_FILE = '/opt/share/enforcer/enforcer.conf'
Message = namedtuple('Message', ['subject', 'body'])
USER_ERROR_INFO = dedent("""\
Username: {user}
Time: {time}
File: {doc_name}
Total pages: {pages}
Pages left today: {daily_pages}
Pages left this semester: {semester_pages}\
""")
INSUFFICIENT_QUOTA_MESSAGE = Message(
subject='[OCF] Your latest print job was rejected',
body=dedent("""\
Greetings from the Open Computing Facility,
This email is letting you know that your most recent print job was
rejected since it would exceed your daily quota. The daily quota is
{daily_quota} pages today and the semesterly quota is {semester_quota} pages.
""") + USER_ERROR_INFO + dedent("""
Does something look wrong? Please reply to
help@ocf.berkeley.edu
""") + MAIL_SIGNATURE
)
PRINTER_ERROR_MESSAGE = Message(
subject='[OCF] Your latest print job failed',
body=dedent("""\
Greetings from the Open Computing Facility,
This email is from the OCF to let you know that your most recent print
job failed due to a printer error. If there's something wrong with the
printers, please alert the publications staff at the desk.
""") + USER_ERROR_INFO + dedent("""
Still can't get it to print? Please reply to
help@ocf.berkeley.edu
""") + MAIL_SIGNATURE
)
ENFORCER_ERROR_MESSAGE = Message(
subject='[OCF] Your latest print job failed',
body=dedent("""\
Greetings from the Open Computing Facility,
This email is from the OCF to let you know that your most recent print
job failed due to a problem with the print accounting system. OCF staff
have been notified of the problem and should fix it shortly. If there
is a staff member in lab, you can ask them for help in the meantime.
""") + USER_ERROR_INFO + dedent("""
Still can't get it to print? Please reply to
help@ocf.berkeley.edu
""") + MAIL_SIGNATURE
)
NOTIFY_QUOTA_MESSAGE = dedent("""\
Your print job failed due to insufficient pages. Your job was
{pages} pages, and you have {quota} pages remaining today.\
""")
NOTIFY_JOB_ACCEPTED = dedent("""\
Your print job '{document}' was accepted and sent to {printer}.\
""")
NOTIFY_JOB_ERROR = dedent("""\
Your print job '{document}' failed due to a printer error.
Please contact a staff member for assistance.\
""")
NOTIFY_PROBLEMATIC_FILE_SOURCE = dedent("""\
Unfortunately, printing from {file_source} results in formatting issues.
Please download your file as a PDF and print from desktop instead of web browser.\
""")
PROBLEMATIC_FILE_SOURCE = [
('Google Docs', lambda job: job.doc_name.endswith(' - Google Docs'))
]
def read_config():
conf = ConfigParser()
conf.read(CONF_FILE)
mysql_user = conf.get('mysql', 'user')
mysql_passwd = conf.get('mysql', 'password')
redis_host = conf.get('broker', 'host')
redis_passwd = conf.get('broker', 'password')
return mysql_user, mysql_passwd, redis_host, redis_passwd
def page_count(env):
path = env['TEADATAFILE']
num_copy = int(env['TEACOPIES'])
return num_copy * int(subprocess.check_output(('/usr/local/bin/enforcer-pc', path), timeout=30))
def create_job(env):
printer = env['TEAPRINTERNAME']
queue = env['CLASS']
return quota.Job(
user=env['TEAUSERNAME'],
time=datetime.now(),
pages=page_count(env),
queue=queue,
printer=printer,
doc_name=env['TEATITLE'],
filesize=env['TEAJOBSIZE'],
)
def send_printer_mail(message, job, quo):
body = message.body.format(
user=job.user,
time=job.time,
doc_name=job.doc_name,
pages=job.pages,
daily_pages=quo.daily,
semester_pages=quo.semesterly,
daily_quota=quota.daily_quota(),
semester_quota=quota.SEMESTERLY_QUOTA,
)
send_mail_user(job.user, message.subject, body)
def prehook(c, r, job):
quo = quota.get_quota(c, job.user)
# A stop gap until we figure out why some file types aren't printing correctly
for file_source, problematic in PROBLEMATIC_FILE_SOURCE:
if problematic(job):
msg = NOTIFY_PROBLEMATIC_FILE_SOURCE.format(
file_source=file_source,
)
r.publish(job.user, msg)
sys.exit(255)
if job.pages > quo.daily:
send_printer_mail(INSUFFICIENT_QUOTA_MESSAGE, job, quo)
msg = NOTIFY_QUOTA_MESSAGE.format(
pages=job.pages,
quota=quo.daily,
)
r.publish(job.user, msg)
sys.exit(255)
def posthook(c, r, job, success):
msg = ''
if success:
quota.add_job(c, job)
msg = NOTIFY_JOB_ACCEPTED.format(
document=job.doc_name,
printer=job.printer.split('-')[0],
)
else:
quo = quota.get_quota(c, job.user)
msg = NOTIFY_JOB_ERROR.format(document=job.doc_name)
send_printer_mail(PRINTER_ERROR_MESSAGE, job, quo)
r.publish(job.user, msg)
def main(argv):
job, quo = None, None
try:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument('command',
choices={'prehook', 'posthook'})
args = parser.parse_args(argv[1:])
job = create_job(os.environ)
mysql_user, mysql_pass, redis_host, redis_pass = read_config()
r = redis.StrictRedis(host=redis_host, port=6378, password=redis_pass, ssl=True)
with quota.get_connection(user=mysql_user, password=mysql_pass) as c:
if args.command == 'prehook':
prehook(c, r, job)
else:
success = os.environ['TEASTATUS'] == '0'
posthook(c, r, job, success)
except SystemExit as e:
sys.exit(e.code)
except Exception:
msg = dedent("""\
enforcer encountered the following error while processing a job:
{traceback}
tea4cups environment variables:
{vars}
""").format(
traceback=format_exc(),
# seems unlikely a non-tea4cups var will start with TEA
vars='\n'.join(' {}: {}'.format(k, v) for k, v in
os.environ.items() if k.startswith('TEA'))
)
syslog(msg)
send_problem_report(msg)
if job and args.command == 'prehook':
try:
send_printer_mail(
ENFORCER_ERROR_MESSAGE,
job,
quo or quota.UserQuota(user=job.user, daily='Unknown',
semesterly='Unknown')
)
except Exception:
pass
# Don't retry; it's not going to print the second time.
sys.exit(255)
if __name__ == '__main__':
main(sys.argv)
You can’t perform that action at this time.