Skip to content

Instantly share code, notes, and snippets.

@cvanelteren
Created June 28, 2026 23:06
Show Gist options
  • Select an option

  • Save cvanelteren/4309d17c1ac95aa9ad596b0853adb7a6 to your computer and use it in GitHub Desktop.

Select an option

Save cvanelteren/4309d17c1ac95aa9ad596b0853adb7a6 to your computer and use it in GitHub Desktop.
printing at qut
#!/usr/bin/env bash
set -euo pipefail
queue_name="QUT-Print"
queue_name_set=0
printer_uri=""
set_default=1
dry_run=0
test_print=0
fix_dns=0
diagnose_only=0
usage() {
cat <<'EOF'
Usage: scripts/setup_qut_printing.sh [options]
Set up QUT printing through CUPS using IPP/IPP Everywhere discovery.
Run this on the host desktop while connected to QUT Wi-Fi or QUT VPN.
Options:
--queue-name NAME Local CUPS queue name, default: QUT-Print
--uri URI Skip discovery and add this IPP/IPPS URI directly
SMB URIs are also accepted, e.g. smb://server/share
--qut-student-bw Use QUT Student Mobile Black and White
--qut-student-colour
Use QUT Student Mobile Colour
--qut-staff Use QUT Staff FindMe
--qut-staff-bw Use QUT Staff FindMe Black and White
--no-default Do not set the QUT queue as the default printer
--test-print Send a CUPS test page after adding the queue
--fix-dns Force the active NetworkManager Wi-Fi connection to use QUT DNS
--diagnose Print DNS, discovery, and endpoint diagnostics, then exit
--dry-run Print actions without changing the system
-h, --help Show this help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--queue-name)
queue_name="${2:?missing queue name}"
queue_name_set=1
shift 2
;;
--uri)
printer_uri="${2:?missing printer URI}"
shift 2
;;
--qut-student-bw)
printer_uri="ipps://ipp.pc-printer-discovery.qut.edu.au:9164/printers/Student+Mobile+Black+and+White"
if [[ "$queue_name_set" -eq 0 ]]; then
queue_name="QUT-Student-BW"
fi
shift
;;
--qut-student-colour|--qut-student-color)
printer_uri="ipps://ipp.pc-printer-discovery.qut.edu.au:9164/printers/Student+Mobile+Colour"
if [[ "$queue_name_set" -eq 0 ]]; then
queue_name="QUT-Student-Colour"
fi
shift
;;
--qut-staff)
printer_uri="ipps://ipp.pc-printer-discovery-1.qut.edu.au:9164/printers/FindMe"
if [[ "$queue_name_set" -eq 0 ]]; then
queue_name="QUT-Staff-FindMe"
fi
shift
;;
--qut-staff-bw)
printer_uri="ipps://ipp.pc-printer-discovery-1.qut.edu.au:9164/printers/FindMe+%28Black+%26+White%29"
if [[ "$queue_name_set" -eq 0 ]]; then
queue_name="QUT-Staff-BW"
fi
shift
;;
--no-default)
set_default=0
shift
;;
--test-print)
test_print=1
shift
;;
--fix-dns)
fix_dns=1
shift
;;
--diagnose)
diagnose_only=1
shift
;;
--dry-run)
dry_run=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
have() {
command -v "$1" >/dev/null 2>&1
}
run() {
if [[ "$dry_run" -eq 1 ]]; then
printf '+'
printf ' %q' "$@"
printf '\n'
else
"$@"
fi
}
as_root() {
if [[ "$(id -u)" -eq 0 ]]; then
run "$@"
elif have sudo; then
run sudo "$@"
else
echo "Need root privileges for: $*" >&2
echo "Install sudo or rerun this command as root." >&2
exit 1
fi
}
detect_os_id() {
if [[ -r /etc/os-release ]]; then
. /etc/os-release
printf '%s\n' "${ID:-unknown}"
else
printf 'unknown\n'
fi
}
install_command() {
case "$(detect_os_id)" in
arch|endeavouros|manjaro)
printf 'sudo pacman -S --needed cups cups-filters avahi nss-mdns system-config-printer\n'
;;
debian|ubuntu|linuxmint|pop)
printf 'sudo apt update && sudo apt install cups cups-browsed avahi-daemon printer-driver-all system-config-printer\n'
;;
fedora)
printf 'sudo dnf install cups cups-filters avahi nss-mdns system-config-printer\n'
;;
opensuse*|suse)
printf 'sudo zypper install cups cups-filters avahi nss-mdns system-config-printer\n'
;;
*)
printf 'Install CUPS, Avahi/mDNS, cups-filters, and system-config-printer with your distro package manager.\n'
;;
esac
}
require_commands() {
local missing=()
for cmd in lpadmin lpstat lpinfo ippfind driverless; do
if ! have "$cmd"; then
missing+=("$cmd")
fi
done
if [[ "${#missing[@]}" -gt 0 ]]; then
echo "Missing required print tools: ${missing[*]}" >&2
echo "Suggested install command:" >&2
install_command >&2
exit 1
fi
}
service_exists() {
systemctl list-unit-files "$1" >/dev/null 2>&1
}
cups_unit() {
if service_exists cups.service; then
printf 'cups.service\n'
elif service_exists org.cups.cupsd.service; then
printf 'org.cups.cupsd.service\n'
else
printf 'cups.service\n'
fi
}
start_services() {
if ! have systemctl; then
echo "systemctl not found; start CUPS and Avahi manually, then rerun." >&2
exit 1
fi
local cups
cups="$(cups_unit)"
echo "Starting print discovery services: $cups and avahi-daemon.service"
as_root systemctl enable --now "$cups"
as_root systemctl enable --now avahi-daemon.service
}
show_network_hint() {
echo "Network DNS context:"
if [[ -r /etc/resolv.conf ]]; then
sed -n '1,12p' /etc/resolv.conf | sed 's/^/ /'
fi
echo
diagnose_dns_config
echo "If discovery still finds no QUT queue, connect to QUT Wi-Fi or QUT VPN and rerun this script."
echo "If you are already on QUT/eduroam, rerun with --fix-dns:"
echo " scripts/setup_qut_printing.sh --fix-dns --test-print"
}
diagnose_dns_config() {
if [[ ! -r /etc/resolv.conf ]]; then
return
fi
local first_nameserver search_line
first_nameserver="$(awk '$1 == "nameserver" {print $2; exit}' /etc/resolv.conf)"
search_line="$(awk '$1 == "search" {$1=""; sub(/^ /, ""); print; exit}' /etc/resolv.conf)"
if [[ "$search_line" =~ (^|[[:space:]])[0-9]+(\.[0-9]+){3}($|[[:space:]]) ]]; then
echo "DNS issue: the search domain list contains an IP address:"
echo " search $search_line"
echo "NetworkManager DNS search entries should be domains only, e.g. qut.edu.au."
echo
fi
case "$first_nameserver" in
131.181.127.32|131.181.59.48)
;;
"")
;;
*)
if grep -qE '^nameserver[[:space:]]+(131\.181\.127\.32|131\.181\.59\.48)$' /etc/resolv.conf; then
echo "DNS issue: QUT DNS is present, but it is not first."
echo "Internal QUT hostnames can fail if public DNS answers first."
echo
fi
;;
esac
}
active_wifi_connection() {
if ! have nmcli; then
return 1
fi
nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status \
| awk -F: '$2 == "wifi" && $3 == "connected" && $4 != "" {print $4; exit}'
}
fix_qut_dns() {
if ! have nmcli; then
echo "nmcli not found; cannot update NetworkManager DNS settings." >&2
exit 1
fi
local connection
connection="$(active_wifi_connection 2>/dev/null || true)"
if [[ -z "$connection" ]]; then
if [[ "$dry_run" -eq 1 ]]; then
connection="<active-wifi-connection>"
else
echo "Could not identify an active Wi-Fi NetworkManager connection." >&2
echo "Run 'nmcli connection show --active' and set QUT DNS on the eduroam/QUT connection manually." >&2
exit 1
fi
fi
echo "Setting QUT DNS on active Wi-Fi connection: $connection"
as_root nmcli connection modify "$connection" \
ipv4.ignore-auto-dns yes \
ipv4.dns "131.181.127.32 131.181.59.48" \
ipv4.dns-search "qut.edu.au" \
ipv6.ignore-auto-dns yes
echo "Reconnecting $connection to apply DNS changes"
as_root nmcli connection down "$connection"
as_root nmcli connection up "$connection"
if have resolvectl; then
run resolvectl flush-caches >/dev/null 2>&1 || true
fi
}
discover_uris() {
{
dns_sd_service_uris
driverless 2>/dev/null || true
ippfind -T 8 _ipp._tcp --print 2>/dev/null || true
ippfind -T 8 _ipps._tcp --print 2>/dev/null || true
ippfind -T 8 _ipp._tcp.qut.edu.au. --print 2>/dev/null || true
ippfind -T 8 _ipps._tcp.qut.edu.au. --print 2>/dev/null || true
} | awk '/^ipps?:\/\// {print}' | sort -u
}
probe_mobility_print_setup() {
if ! have curl; then
return 0
fi
local urls=(
"https://mobilityprint.qut.edu.au:9164/setup"
"https://print.qut.edu.au:9164/setup"
"https://printing.qut.edu.au:9164/setup"
"https://myprint.qut.edu.au:9164/setup"
"https://papercut.qut.edu.au:9164/setup"
)
local url
for url in "${urls[@]}"; do
if curl -fsSIk --connect-timeout 4 --max-time 8 "$url" >/dev/null 2>&1; then
echo
echo "Found a reachable PaperCut Mobility Print setup page:"
echo " $url"
if have xdg-open; then
echo "Opening it in your browser. Choose the Linux setup option if direct IPP discovery is not enough."
run xdg-open "$url" >/dev/null 2>&1 || true
fi
return 0
fi
done
}
qut_candidate_hosts() {
cat <<'EOF'
mobilityprint.qut.edu.au
print.qut.edu.au
qpspcmobile01.qut.edu.au
pc-printer-discovery.qut.edu.au
pc-printer-discovery-1.qut.edu.au
printing.qut.edu.au
myprint.qut.edu.au
webprint.qut.edu.au
papercut.qut.edu.au
printdeploy.qut.edu.au
secureprint.qut.edu.au
qutprint.qut.edu.au
studentprint.qut.edu.au
staffprint.qut.edu.au
printserver.qut.edu.au
printers.qut.edu.au
cups.qut.edu.au
EOF
}
resolve_host() {
local host="$1"
local out=""
if have dig; then
out="$(
dig +time=1 +tries=1 +short "$host" A 2>/dev/null
dig +time=1 +tries=1 +short "$host" AAAA 2>/dev/null
)"
elif have drill; then
out="$(
drill -Q -t A "$host" 2>/dev/null
drill -Q -t AAAA "$host" 2>/dev/null
)"
elif have host && have timeout; then
out="$(timeout 2 host "$host" 2>/dev/null | awk '/has address|has IPv6 address/ {print $NF}')"
elif have nslookup && have timeout; then
out="$(timeout 2 nslookup "$host" 2>/dev/null | awk '/^Address: / {print $2}')"
elif have timeout; then
out="$(timeout 2 getent ahosts "$host" 2>/dev/null | awk '{print $1}')"
fi
printf '%s\n' "$out" | sed '/^[[:space:]]*$/d' | sort -u | paste -sd ' ' -
}
query_dns_record() {
local name="$1"
local type="$2"
if have dig; then
dig +time=2 +tries=1 +short "$name" "$type" 2>/dev/null
elif have drill; then
drill -Q -t "$type" "$name" 2>/dev/null
elif have resolvectl; then
resolvectl query --type="$type" "$name" 2>/dev/null \
| awk '/--/ {next} /IN[[:space:]]+'"${type}"'/ {print}'
else
return 0
fi
}
dns_sd_service_uris() {
local domain regtype scheme service srv priority weight port target txt rp
while IFS= read -r domain; do
for regtype in "_ipps._tcp.$domain" "_ipp._tcp.$domain"; do
case "$regtype" in
_ipps.*) scheme="ipps" ;;
*) scheme="ipp" ;;
esac
while IFS= read -r service; do
service="${service%.}"
[[ -z "$service" ]] && continue
srv="$(query_dns_record "$service" SRV | head -n 1)"
txt="$(query_dns_record "$service" TXT | tr -d '"')"
rp="$(printf '%s\n' "$txt" | grep -oE '(^|[[:space:]])rp=[^[:space:]]+' | head -n 1 | sed 's/^ *rp=//')"
if [[ -z "$srv" || -z "$rp" ]]; then
continue
fi
read -r priority weight port target <<<"$srv"
target="${target%.}"
[[ -z "$port" || -z "$target" ]] && continue
printf '%s://%s:%s/%s\n' "$scheme" "$target" "$port" "$rp"
done < <(query_dns_record "$regtype" PTR)
done
done < <(qut_browse_domains)
}
qut_browse_domains() {
{
printf 'qut.edu.au\n'
query_dns_record b._dns-sd._udp.qut.edu.au PTR
query_dns_record lb._dns-sd._udp.qut.edu.au PTR
} | sed 's/\.$//' | sed '/^[[:space:]]*$/d' | sort -u
}
qut_dns_sd_records() {
cat <<'EOF'
b._dns-sd._udp.qut.edu.au PTR
db._dns-sd._udp.qut.edu.au PTR
lb._dns-sd._udp.qut.edu.au PTR
_printer._tcp.qut.edu.au PTR
EOF
local domain
while IFS= read -r domain; do
printf '_ipp._tcp.%s PTR\n' "$domain"
printf '_ipps._tcp.%s PTR\n' "$domain"
printf '_universal._sub._ipp._tcp.%s PTR\n' "$domain"
printf '_universal._sub._ipps._tcp.%s PTR\n' "$domain"
done < <(qut_browse_domains)
}
probe_url() {
local url="$1"
if ! have curl; then
return
fi
local code
code="$(curl -kLsS -o /dev/null -w '%{http_code}' --connect-timeout 1 --max-time 2 "$url" 2>/dev/null || true)"
case "$code" in
000|"")
;;
*)
printf ' %-44s HTTP %s\n' "$url" "$code"
;;
esac
}
diagnose_qut_printing() {
echo "== DNS =="
show_network_hint
echo
echo "== Candidate hostnames =="
local host resolved any_host=0
while IFS= read -r host; do
printf ' %-32s ' "$host"
resolved="$(resolve_host "$host")"
if [[ -n "$resolved" ]]; then
any_host=1
printf '%s\n' "$resolved"
else
printf 'not found\n'
fi
done < <(qut_candidate_hosts)
if [[ "$any_host" -eq 0 ]]; then
echo " No common QUT print hostnames resolved."
fi
echo
echo "== Printer discovery =="
local uris=()
mapfile -t uris < <(discover_uris)
if [[ "${#uris[@]}" -eq 0 ]]; then
echo " No IPP/DNS-SD printers discovered."
else
printf ' %s\n' "${uris[@]}"
fi
echo
echo "== QUT DNS-SD records =="
local dns_name dns_type dns_answer any_dns=0 service service_answer
while read -r dns_name dns_type; do
dns_answer="$(query_dns_record "$dns_name" "$dns_type" | sed '/^[[:space:]]*$/d')"
if [[ -n "$dns_answer" ]]; then
any_dns=1
printf ' %s %s\n' "$dns_name" "$dns_type"
printf '%s\n' "$dns_answer" | sed 's/^/ /'
if [[ "$dns_type" == "PTR" && "$dns_name" =~ ^_ipps?\._tcp\. ]]; then
while IFS= read -r service; do
service="${service%.}"
[[ -z "$service" ]] && continue
service_answer="$(query_dns_record "$service" SRV | sed '/^[[:space:]]*$/d')"
if [[ -n "$service_answer" ]]; then
printf ' %s SRV\n' "$service"
printf '%s\n' "$service_answer" | sed 's/^/ /'
fi
service_answer="$(query_dns_record "$service" TXT | sed '/^[[:space:]]*$/d')"
if [[ -n "$service_answer" ]]; then
printf ' %s TXT\n' "$service"
printf '%s\n' "$service_answer" | sed 's/^/ /'
fi
done <<<"$dns_answer"
fi
else
printf ' %-44s no %s record\n' "$dns_name" "$dns_type"
fi
done < <(qut_dns_sd_records)
if [[ "$any_dns" -eq 0 ]]; then
echo " No QUT DNS-SD printer records found."
fi
echo
echo "== Likely web endpoints =="
local any_url=0
while IFS= read -r host; do
if [[ -n "$(resolve_host "$host")" ]]; then
for url in \
"https://$host:9164/setup" \
"http://$host:9163/setup" \
"https://$host:9192/user" \
"http://$host:9191/user" \
"https://$host/"; do
local probe
probe="$(probe_url "$url")"
if [[ -n "$probe" ]]; then
any_url=1
printf '%s\n' "$probe"
fi
done
fi
done < <(qut_candidate_hosts)
if [[ "$any_url" -eq 0 ]]; then
echo " No common PaperCut/Mobility Print web endpoints responded."
fi
echo
echo "Send the output above when asking HiQ/IT. The key request is:"
echo " What is the Linux/CUPS URI or PaperCut Mobility Print setup URL for QUT printing?"
}
choose_uri() {
local candidates=()
mapfile -t candidates < <(discover_uris)
if [[ "${#candidates[@]}" -eq 0 ]]; then
echo "No IPP printers discovered." >&2
probe_mobility_print_setup
show_network_hint >&2
exit 1
fi
local preferred=()
local rest=()
local uri
for uri in "${candidates[@]}"; do
if [[ "$uri" =~ [Qq][Uu][Tt]|[Pp]aper[Cc]ut|[Mm]obility|qut\.edu\.au ]]; then
preferred+=("$uri")
else
rest+=("$uri")
fi
done
candidates=("${preferred[@]}" "${rest[@]}")
if [[ "${#candidates[@]}" -eq 1 ]]; then
printer_uri="${candidates[0]}"
return
fi
echo "Discovered IPP printers:"
local i
for i in "${!candidates[@]}"; do
printf ' %2d) %s\n' "$((i + 1))" "${candidates[$i]}"
done
local choice
read -r -p "Select the QUT/PaperCut printer number: " choice
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#candidates[@]} )); then
echo "Invalid selection." >&2
exit 1
fi
printer_uri="${candidates[$((choice - 1))]}"
}
add_queue() {
case "$printer_uri" in
""|*...*|ipp://|ipps://|smb://)
echo "Invalid printer URI: $printer_uri" >&2
echo "Use a real QUT printer URI, not the placeholder. Examples:" >&2
echo " ipps://print-host.qut.edu.au/printers/queue-name" >&2
echo " smb://print-host.qut.edu.au/queue-name" >&2
echo "If you do not have the URI, run:" >&2
echo " scripts/setup_qut_printing.sh --diagnose" >&2
exit 2
;;
esac
echo "Adding CUPS queue '$queue_name'"
echo "URI: $printer_uri"
local model="everywhere"
case "$printer_uri" in
ipp://*|ipps://*)
model="everywhere"
;;
smb://*)
model="$(lpinfo -m 2>/dev/null | awk '/^drv:\/\/\/sample\.drv\/generic\.ppd / {print $1; found=1; exit} END {if (!found) exit 1}' || true)"
if [[ -z "$model" ]]; then
model="$(lpinfo -m 2>/dev/null | awk 'tolower($0) ~ /generic.*postscript/ {print $1; found=1; exit} END {if (!found) exit 1}' || true)"
fi
if [[ -z "$model" ]]; then
model="raw"
fi
if [[ ! -x /usr/lib/cups/backend/smb && ! -x /usr/libexec/cups/backend/smb ]]; then
echo "Warning: CUPS SMB backend was not found. On Arch install samba if this queue fails." >&2
fi
;;
esac
as_root lpadmin \
-p "$queue_name" \
-E \
-v "$printer_uri" \
-m "$model" \
-o printer-is-shared=false \
-o auth-info-required=username,password
as_root cupsenable "$queue_name"
as_root cupsaccept "$queue_name"
if [[ "$set_default" -eq 1 ]]; then
as_root lpadmin -d "$queue_name"
fi
echo "Queue added. Your first print job should prompt for QUT username/password."
}
test_page() {
local test_file="/usr/share/cups/data/testprint"
if [[ ! -r "$test_file" ]]; then
echo "CUPS test page not found at $test_file; skipping test print." >&2
return
fi
echo "Sending CUPS test page to $queue_name"
run lp -d "$queue_name" "$test_file"
}
require_commands
if [[ "$fix_dns" -eq 1 ]]; then
fix_qut_dns
fi
start_services
if [[ "$diagnose_only" -eq 1 ]]; then
diagnose_qut_printing
exit 0
fi
if [[ -z "$printer_uri" ]]; then
choose_uri
fi
add_queue
if [[ "$test_print" -eq 1 ]]; then
test_page
fi
echo
lpstat -v "$queue_name" 2>/dev/null || true
lpstat -p "$queue_name" 2>/dev/null || true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment