Perl automated pipeline for ingesting school district student data into SirsiDynix Symphony ILS.
Author: John Houser (john.houser@multco.us)
Copyright: Multnomah County 2025
- Overview
- Architecture & Data Flow
- Directory Structure
- Prerequisites & Dependencies
- Installation & Setup
- Utility Scripts (
bin/) - Configuration Reference (
config.yaml) - Field Definition Keywords & Validation Rules
- Logging & Error Handling
ReLibConnectEd (librelibconnected) monitors for school district student data CSV files uploaded via SFTP into subdirectories under /srv/libconnected.
Upon detecting an uploaded CSV file, the application:
- Validates the data schema and applies field-level validation and truncation rules.
- Formats and standardizes address components to conform with USPS standards (via
AddressFormat.pm). - Computes MD5 checksums (digests) for student records and compares them against a local MySQL database. If a student's data hasn't changed since the previous load, processing for that student is skipped.
- Performs multi-tier patron searches and updates or creates patron records in SirsiDynix Symphony using the SirsiDynix Web Services API (
ILSWS.pm). - Generates run statistics and sends an email report (with log attachments via
MIME::Lite) to administrators and district contacts. - Cleans up ingested files and temporary logs.
[ School District ] --(SFTP Upload)--> /srv/libconnected/<district>/incoming/*.csv
|
(Cron Trigger)
v
relibconnected.pl
(Atomic Locking & Taint Mode)
|
v
ingestor.pl
|
+-------------------------------+-------------------------------+
| | |
v v v
AddressFormat.pm MySQL Database ILSWS.pm
(USPS Address Formatting) (MD5 Checksum Matching) (SirsiDynix Symphony API)
| | |
+-------------------------------+-------------------------------+
|
v
MIME::Lite
(Email Ingest Report & Logs)
-
relibconnected.pl: The cron-driven wrapper entrypoint.- Operates under Perl Taint mode (
-T). - Uses atomic lock creation (
sysopenwithO_CREAT|O_EXCLand advisoryflock) at/opt/librelibconnected/run/ingestor.lockto prevent concurrent process collisions. - Scans
/srv/libconnectedfor incoming CSV files and safely invokesingestor.plusing list-formsystem()execution to prevent shell injection.
- Operates under Perl Taint mode (
-
ingestor.pl: The primary data ingestion engine.- Loads
config.yamland initializes Log4perl logging. - Maps incoming CSV headers to the configured schema (
districtorpps). - Connects to SirsiDynix Symphony ILSWS and the local MySQL checksum database.
- Processes student records line-by-line: validates, formats addresses, computes MD5 digests, searches for matching patrons, updates existing records or creates new ones.
- Emails run reports with attached logs (
mail.logandingestor.csv) to configured admin and district contacts. - Safely deletes temporary log files and the processed CSV file.
- Loads
-
ILSWS.pm: Custom API client for SirsiDynix Web Services.- Handles authentication, session tokens, patron searches (Alt ID, Email, Barcode, DOB + Street), patron creation, and patron updates.
-
AddressFormat.pm: Package for standardizing street address strings to USPS standards.- Handles trimming whitespace, stripping non-standard punctuation, standardizing compass directionals (e.g., "N", "SE"), abbreviating street types (e.g., "Avenue" -> "Ave"), normalizing apartment/unit designations, and title-casing street names.
-
DataHandler.pm: Package for evaluating mapping tables and validating dates, strings, numbers, and ranges.
Default installation path: /opt/librelibconnected
/opt/librelibconnected/
├── AddressFormat.pm # USPS address formatting module
├── DataHandler.pm # Data mapping and validation module
├── ILSWS.pm # SirsiDynix Web Services API wrapper
├── README.md # System documentation
├── config.yaml # Application configuration file
├── config.yaml.sample # Sample configuration template
├── ingestor.pl # Main CSV ingestion engine
├── log.conf # Log4perl configuration file
├── relibconnected.pl # Secure wrapper / cron entrypoint
├── bin/ # Administrative and utility scripts
│ ├── create_checksum_db.pl # Database initialization script
│ ├── create_new_user.pl # District SFTP user setup script
│ ├── randomize_checksum_ages.pl# Digest expiration jitter tool
│ ├── remove_old_checksums.pl # Expired digest purge tool
│ ├── report_checksum_dates.pl # Digest distribution reporting tool
│ ├── stuck_clear.pl # Stuck flag and stale log recovery
│ ├── stuck_warn.pl # Stale file monitoring and alerting
│ ├── stuck_clear.txt # Notification template for stuck_clear
│ └── stuck_warn.txt # Notification template for stuck_warn
├── log/ # Application log directory
│ ├── ingestor.csv # Per-run CSV audit log (auto-generated)
│ └── mail.log # Per-run email log summary (auto-generated)
└── run/ # Process lock file directory
└── ingestor.lock # Atomic lock file created during ingest
SFTP Directory Structure: /srv/libconnected
/srv/libconnected/
├── <namespace><id>/ # e.g., pps40, multco03
│ ├── .ssh/
│ │ └── authorized_keys # District SSH key
│ └── incoming/ # Target upload directory for district CSV files
This software is designed for Ubuntu Server (or Debian-based Linux systems). The following OS packages are required:
sudo apt update
sudo apt install mysql-server libssl-dev zlib1g-dev sendmailAfter installing mysql-server, secure the installation and note the root password:
sudo mysql_secure_installationThe application requires Perl 5.10+ and the following CPAN modules:
sudo cpan install Data::Dumper Date::Calc DBI DBD::mysql Digest::MD5 \
Email::Valid File::Basename File::Find HTTP::Request JSON \
Log::Log4perl LWP::Protocol::https LWP::UserAgent MIME::Lite \
Parse::CSV Readonly Text::CSV_XS Try::Tiny Unicode::Normalize URI YAML::Tiny-
Deploy Application Directory: Copy all repository files to
/opt/librelibconnected(or your preferred base path):sudo mkdir -p /opt/librelibconnected sudo cp -r * /opt/librelibconnected/ -
Configure Modules in Perl Path: Copy
AddressFormat.pm,DataHandler.pm, andILSWS.pmto/usr/local/lib/site_perl/or ensure/opt/librelibconnectedis included in Perl's@INCpath:sudo cp AddressFormat.pm DataHandler.pm ILSWS.pm /usr/local/lib/site_perl/
-
Set Up Logging Directory: Create the system log directory and set permissions for the service user running the application:
sudo mkdir -p /var/log/relibconnected sudo chown -R libconnected:libconnected /var/log/relibconnected
-
Configure
config.yaml: Copyconfig.yaml.sampletoconfig.yamland update values for admin contacts, ILSWS API credentials, MySQL credentials, SMTP settings, and district configurations:cd /opt/librelibconnected cp config.yaml.sample config.yaml nano config.yaml -
Initialize MySQL Digest Database: Run
create_checksum_db.plas root, supplying the config file path and MySQL root password:sudo bin/create_checksum_db.pl /opt/librelibconnected/config.yaml 'MYSQL_ROOT_PASSWORD' -
Create District SFTP Accounts & Upload Directories: Run
create_new_user.plas root to configure district system accounts and/srv/libconnected/<district>/incomingdirectories:sudo bin/create_new_user.pl /opt/librelibconnected/config.yaml
-
Configure SSH SFTP Chroot: Restrict district users to SFTP access only by adding the following stanza at the bottom of
/etc/ssh/sshd_configand restarting the SSH service (sudo systemctl restart ssh):Match group sftponly ChrootDirectory /srv/libconnected X11Forwarding no AllowTcpForwarding no ForceCommand internal-sftp -u 0117 -
Schedule Cron Jobs: Set up crontab entries for the service user running
relibconnected:# Check for incoming student data files every 5 minutes */5 * * * * /opt/librelibconnected/relibconnected.pl # Purge expired checksum digests daily at 2:15 AM 15 2 * * * /opt/librelibconnected/bin/remove_old_checksums.pl /opt/librelibconnected/config.yaml # Check for stuck incoming files (>90 mins) every hour 0 * * * * /opt/librelibconnected/bin/stuck_warn.pl
The bin/ directory contains administrative tools:
| Script | Execution Context | Usage / Syntax | Description |
|---|---|---|---|
create_checksum_db.pl |
Setup (Root) | sudo bin/create_checksum_db.pl CONFIG_FILE MYSQL_ROOT_PASS |
Creates the MySQL database, application user, grants privileges, and initializes the checksums table. |
create_new_user.pl |
Setup (Root) | sudo bin/create_new_user.pl CONFIG_FILE |
Creates district system user accounts, sftponly group, directory trees under /srv/libconnected, and SSH authorized_keys. |
remove_old_checksums.pl |
Cron / Admin | bin/remove_old_checksums.pl CONFIG_FILE |
Deletes MD5 checksum entries older than max_checksum_age (from config.yaml), forcing full re-checks on next upload. |
randomize_checksum_ages.pl |
Admin | bin/randomize_checksum_ages.pl CONFIG_FILE |
Randomly adjusts date_added in checksums by up to ±29 days to stagger record expiration dates and avoid update spikes. |
report_checksum_dates.pl |
Admin | bin/report_checksum_dates.pl CONFIG_FILE |
Summarizes and prints record count totals grouped by date_added in the checksum database. |
stuck_warn.pl |
Cron / Admin | bin/stuck_warn.pl |
Scans /srv/libconnected for incoming CSV files older than 90 minutes and emails an administrative warning. |
stuck_clear.pl |
Admin | bin/stuck_clear.pl |
Recovers from stale lock/flag states, archives older logs with timestamps, and notifies administration. |
admin_contact: Admin Contact <admin@example.org> # Email address for system alerts
base_path: /opt/librelibconnected # Application base directory
incoming_path: /srv/libconnected # SFTP base directory
log_level: info # Logging verbosity (debug|info|warn|error|fatal)
service_account: libconnected # Linux service user owning incoming folders
adult_profile: 0_MULT # Default adult patron profile ID
ilsws: # SirsiDynix Web Services API credentials
username: API_USER
password: API_PASSWORD
hostname: ilsws.example.org
port: 443
webapp: symphony
client_id: CLIENT_ID
app_id: relibconnected
user_privilege_override: OVERRIDE
timeout: 40
max_retries: 3
mysql: # Local Digest Database settings
hostname: localhost
port: 3306
db_name: libconnected_checksums
db_username: libconnected
db_password: DB_PASSWORD
max_checksum_age: 90 # Digest expiration in days
smtp: # Mail transfer settings
hostname: smtp.example.org
port: 25
from: libconnected@example.org
user: ''
pass: ''Each district entry under clients: defines authentication, contact information, schema mapping, and field-level rules:
clients:
- id: '40'
authorized_key: "ssh-rsa AAAAB3NzaC1yc2E..."
namespace: pps
schema: district # Schema type: 'district' or 'pps'
name: DDSD
contact: District Contact <district@example.org>
email_reports: false # Set true to send ingest report to client contact
email_pattern: example.org
fields:
barcode:
type: string
overlay: true
validate: s:14
transform: c:transform_barcode
street:
type: address
overlay: true
validate: s:128
transform: c:transform_street # Formats address to USPS standards
new_default: "205 NE Russell St"When configuring client districts in config.yaml, the following keywords define field behavior:
| Keyword | Description |
|---|---|
type |
Symphony field type (determines JSON structure sent to API). |
overlay |
Boolean (true/false). Controls whether this field is updated when an existing patron record is modified. |
validate |
Validation rule string applied to incoming CSV data. Invalid records trigger warnings and skipping. |
transform |
Transformation function in ingestor.pl (e.g., c:transform_street, c:transform_barcode, c:transform_pin, c:transform_profile) which takes input from incoming or existing record data and client configuration, returning a transformed value. |
overlay_default |
Fallback value to insert during an update if the field in Symphony is currently empty. |
overlay_value |
Static value that ALWAYS overwrites the field during an update. |
new_default |
Fallback value to use during new patron creation if the field is empty in incoming data. |
new_value |
Static value ALWAYS used when creating a new patron. |
Validation rules used with the validate keyword:
| Rule Type | Syntax / Example | Description / Constraints |
|---|---|---|
| Date Format 1 | d:YYYY-MM-DD |
ISO date validation (validated against calendar). |
| Date Format 2 | d:YYYY/MM/DD |
Slash-separated ISO date. |
| Date Format 3 | d:MM-DD-YYYY |
US hyphenated date. |
| Date Format 4 | d:MM/DD/YYYY |
US slash-separated date. |
| Timestamp 1 | d:YYYY/MM/DD HH:MM |
Date and time (24h). |
| Timestamp 2 | d:YYYY-MM-DD HH:MM |
ISO date and time. |
| Timestamp 3 | d:YYYYMMDDHHMMSS |
Full numeric timestamp. |
| Integer | i:8 |
Fixed length integer. |
| String | s:256 |
Maximum string character length (e.g. max 256 chars). |
| Value List | v:01|11 |
Pipe-delimited list of allowed literal string values. |
| Blank | b |
Field must be completely blank or empty. |
| Decimal Number | n:3.2 |
Number with specific precision/scale (e.g., 000.00). |
| Integer Range | r:1,9999 |
Integer value bounded inclusively between min and max. |
Logging is driven by Log::Log4perl using the configuration in log.conf:
- System Log: Written to
/var/log/relibconnected/ingestor.log. Logs full operational details, warnings, errors, and debug statements. - Mail Summary Log: Generated per run at
/opt/librelibconnected/log/mail.logand attached to report emails. - CSV Audit Log: Generated per run at
/opt/librelibconnected/log/ingestor.csvdetailing actions taken (create,update,skip) for each record. - Email Notification: On completion,
ingestor.plcompiles the run statistics and emailsmail.logandingestor.csvviaMIME::Lite.