Skip to content

Instantly share code, notes, and snippets.

@agoose77
Last active April 9, 2020 18:22
Show Gist options
  • Save agoose77/365c3c2341efbba4ed5a34ff772538cd to your computer and use it in GitHub Desktop.
Save agoose77/365c3c2341efbba4ed5a34ff772538cd to your computer and use it in GitHub Desktop.
#!/usr/bin/python3
# Originally from from Mark Slater 29/11/16
# Modified for per-user name mapping by Mark Colclough, 28 July 2017
# Modified to suppress SystemExit exception by Angus Hollands, 3 Dec 2019
# Install as /usr/lib/cups/backend/safeq
# Configure a safeq backend in CUPS (lpinfo -v should show it)
# URL/Connection: safeq://localhost (direct)
# PPD: Generic PostScript Printer Foomatic/Postscript
# This script runs as uid 4 (lp) and needs to exec /usr/lib/cups/backend/lpd.
# so check the permissions for this. e.g. lpd in lp group, mode 750.
# Look in syslog (typ /var/log/messages) for chatter. Note DEBUG setting
# below to skip the actual lpd call.
# Also note the dodgy parse and search of the usermap file on each call.
# Single user applications:
# ========================
# (no admin who wants to manage multiple accounts in one config file,
# and each user having their own safeq/ADF account)
# Just set REQUIRE_DOTFILE = True in this file, and put a file .printuser
# in the user's home directory, containing just
# the user's ADF name for printing, e.g. : echo colcloms > ~/.printuser
#
# Admininistered systems
# ======================
# REQUIRE_DOTFILE = False, and create a mapping file,
# /usr/local/etc/usermap.conf, containing
# <UnixID>:<ADFID>
# <UnixID>:<ADFID>
# Any ~/.printuser is still obeyed, but if empty or absent, the
# usermap.conf will be used.
# Multiple print users per unix account
# =====================================
# e.g. hardware controllers
# I have an additional kde desktop applet that lets users manage their
# ~/.printuser file. Also, set WARN_DOTFILE = True below, and
# install my dbus-notify script to complain to the user if the dotfile
# empty or absent
import os
import re
import sys
import random
import syslog
import socket
import grp
import traceback
class SafeQ(object):
USERMAP = '/usr/local/etc/usermap.conf'
REQUIRE_DOTFILE = True
WARN_DOTFILE = True
SAFEQ_PORT = 515
SAFEQ_HOSTS = [ 'its-p-safeq-01.bham.ac.uk', 'its-p-safeq-02.bham.ac.uk', 'its-p-safeq-03.bham.ac.uk', 'its-p-safeq-04.bham.ac.uk' ]
SAFEQ_DEVICE = 'lpd'
SAFEQ_QUEUE = 'secure'
DEBUG = False
def __init__(self):
syslog.openlog('safeQ', syslog.LOG_PID, syslog.LOG_LOCAL6)
uid = os.getuid()
lpid = grp.getgrnam('lp').gr_gid
if uid != lpid :
syslog.syslog(f"{sys.argv[0]}: Needs to run as lp (gid {lpid}), not {uid}")
sys.exit(1)
if len(sys.argv) not in (6, 7) :
syslog.syslog(f"{sys.argv[0]}: job-id user title copies options [file]\n")
sys.exit(1)
def lookup_user_global(self, job_user, job_id):
"""Translate unix user to ADF print user via global USERMAP file.
Returns empty string in case of failure
"""
adf_user = ""
regexp = re.compile('^(\w+)\s*:\s*(\w+)$')
try:
f = open(self.USERMAP, 'r')
for line in f:
if line[0] == '#':
continue
line = line.rstrip()
r = regexp.match(line)
if r is None:
continue
if r.group(1) == job_user:
syslog.syslog("%s: %s - User mapped to %s via %s" % (job_id, job_user, r.group(2), USERMAP))
adf_user = r.group(2)
break
f.close()
except:
pass # any problems, just return empty
return adf_user
def lookup_user_dotfile(self, job_user, job_id):
"""Translate unix user to ADF print user via /home/<UNIXUSER>/.printuser
Returns empty string in case of failure
"""
userfile = "/home/%s/.printuser" % job_user
adf_user = ""
try:
adf_user = open(userfile).read().strip()
syslog.syslog("%s: local user %s mapped to print user %s via %s" % (job_id, job_user, adf_user, userfile))
except:
syslog.syslog("%s: no mapping for local user %s in %s" % (job_id, job_user, userfile))
return adf_user
def lookup_user(self, job_user, job_id):
"""Translate unix user to ADF print user via unix user's .printuser, or
failing that, global USERMAP. Returns empty string if both fail.
"""
job_user = job_user.strip()
adf_user = self.lookup_user_dotfile(job_user, job_id)
if not adf_user:
if self.REQUIRE_DOTFILE:
if self.WARN_DOTFILE:
cmd = 'sudo -u %s dbus-notify "Print user needed" "You must set an ADF user for Safeq central printing"' %(job_user)
os.system(cmd)
syslog.syslog("%s: ~%s/.printuser required but not found" % (job_id, job_user))
else:
syslog.syslog("try global") ## test
adf_user = self.lookup_user_global(job_user, job_id)
return adf_user
def lookup_safeq_dest(self, job_id):
candidates = []
for host in self.SAFEQ_HOSTS:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
try:
s.connect((host, self.SAFEQ_PORT))
except socket.error:
s.close()
continue
candidates.append(host)
s.close()
if len(candidates) == 0:
syslog.syslog('%s: No SafeQ hosts available' % job_id)
sys.exit(2)
safeq_dest = random.choice(candidates)
return safeq_dest
def relay_job(self, safeq_dest, job_user, job_id):
# Generate replacement device uri
safeq_uri = '%s://%s/%s' % (self.SAFEQ_DEVICE, safeq_dest, self.SAFEQ_QUEUE)
os.environ['DEVICE_URI'] = safeq_uri
safeq_backend = os.path.join(os.path.split(sys.argv[0])[0], self.SAFEQ_DEVICE)
# Generate arguments for exec - then replace the job user
# Filenames show up encoded utf8 on ubuntu? so can get error like 'ascii' codec can't decode byte 0xe2 in position 10: ordinal not in range(128)
# workaround by decode-encode which should work without knowing what we were given. Cleaner solution required.
##safeq_args = [safeq_backend] + [a.decode("UTF-8").encode("UTF-8") for a in sys.argv[1:]]
# python3 we hope gets it right
safeq_args = [safeq_backend] + [a for a in sys.argv[1:]]
safeq_args[2] = job_user
if not self.DEBUG:
syslog.syslog('%s: %s - Exec for %s' % (job_id, job_user, safeq_uri))
os.execve(safeq_backend, safeq_args, os.environ)
syslog.syslog('%s: SafeQ job failed to exec' % job_id)
return False
else:
syslog.syslog('%s: SafeQ in debug mode' % job_id)
syslog.syslog('%s: SafeQ debug - User: %s - URI: %s' % (job_id, job_user, safeq_uri))
return True
def run_job(self):
job_id = int(sys.argv[1].strip())
job_user = sys.argv[2].strip()
job_title = sys.argv[3].strip()
job_copies = int(sys.argv[4].strip())
job_options = sys.argv[5].strip()
if len(sys.argv) > 6 :
job_file = sys.argv[6].strip()
else:
job_file = None
syslog.syslog(f"{job_id}: SafeQ job started...")
new_job_user = self.lookup_user(job_user, job_id)
if not new_job_user:
if self.REQUIRE_DOTFILE:
syslog.syslog(f"{job_id}: No .printuser found. Job abandoned")
return True
else:
syslog.syslog(f"{job_id}: No mapping found. Just using this username instead: {job_user}")
new_job_user = job_user
safeq_dest = self.lookup_safeq_dest(job_id)
cmd = 'sudo -u %s dbus-notify "SafeQ print" "Print job sent for user %s to Safeq central printing"' %(job_user, new_job_user)
os.system(cmd)
self.relay_job(safeq_dest, new_job_user, job_id)
return True
if __name__ == "__main__" :
try:
if len(sys.argv) == 1:
# Without arguments should give backend info.
# This is also used when lpinfo -v is issued, where it should include "direct this_backend"
sys.stdout.write("direct %s \"Unknown\" \"Shim for sending jobs to SafeQ system\"\n" % os.path.basename(sys.argv[0]))
sys.stdout.flush()
sys.exit(0)
wrapper = SafeQ()
wrapper.run_job()
sys.exit(0)
except Exception:
exeinfo = sys.exc_info()
exetype = exeinfo[0]
exeinst = exeinfo[1]
tback = exeinfo[2]
syslog.syslog(f'SafeQ backend {exetype}:{exeinst}. Traceback follows.')
for line in traceback.format_tb(tback):
syslog.syslog(line)
sys.exit(1)
@agoose77
Copy link
Author

agoose77 commented Dec 3, 2019

For Ubuntu (19):

  1. Install this script in /usr/lib/cups/backend/:
    cp safeq /usr/lib/cups/backend
  2. Change permissions of the script to 755
  • sudo chown root /usr/lib/cups/backend/safeq
  • sudo chgrp root /usr/lib/cups/backend/safeq
  • sudo chmod 755 /usr/lib/cups/backend/safeq
  1. Ensure the lp user can run the lpd backend, /usr/lib/cups/backend/lpd
  • sudo chgrp lp /usr/lib/cups/backend/lpd
  • sudo chmod g+rx /usr/lib/cups/backend/lpd
    4 lpinfo -v should now show a safeq backend is available - look for the safeq option.
  1. Set up a print queue in CUPS, using
    URL/Connection:  safeq://localhost   (direct)
    PPD: Generic PostScript Printer Foomatic/Postscript 
  1. Add your ADF username to ~/.printuser e.g. echo "exh938" > ~/.printuser

@agoose77
Copy link
Author

agoose77 commented Dec 3, 2019

#!/usr/bin/env sh
wget https://gist.githubusercontent.com/agoose77/365c3c2341efbba4ed5a34ff772538cd/raw/safeq
sudo chown root safeq
sudo chgrp root safeq
sudo chmod 755 safeq
sudo cp safeq /usr/lib/cups/backend

sudo chgrp lp /usr/lib/cups/backend/lpd
sudo chmod g+rx /usr/lib/cups/backend/lpd

if [ -z "$(lpinfo -v | grep "direct safeq")" ]
then
	echo "ERROR: safeq backend does not appear in `lpinfo -v`"
  	exit 1
fi

echo "Now you must add the printer with \nURL/Connection:  safeq://localhost   (direct)\nPPD: Generic PostScript Printer Foomatic/Postscript"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment