Why Scrippy?
Simplify scripting, maintain focus, and uphold quality, reliability and efficiency
Scrippy streamlines scripting with standardized formats, essential features like configuration settings and argument parsing, logging and secrets management, access and concurrent execution control.
Simplifying scripting while maintaining high standards allows for more effective goal achievement by system administrators and developers.
Scrippy is fully open-source
Browse and patch the code, submit pull requests to our repositories at Codeberg.
Argument parsing, configuration checks, logging and locking are handled by Scrippy: your script only contains what it is meant to do
Every Scrippy script follows the same layout and offers the same options (--help, --hist, --log...): anyone in the team can read, run and maintain it
Every execution gets its own log and a line in the script history: who ran it, with which parameters, how long it took and how it ended
Passwords and tokens declared as secrets are redacted from log files, console output and execution history
Only authorized users and groups run your scripts, concurrent executions are kept under control and errors end with a clear message and an exit code, not a stack trace
No new language to learn: the Scrippy header is a plain docstring and your script remains a regular Python file, free to use any Python library
Official modules for SSH/SFTP, FTP and CIFS, databases, REST APIs, mail, Git, SNMP and templates, all sharing Scrippy's conventions
Scrippy is released under the MIT license, so you can spend your money on other stuff
Stay focused on the task
Stop wasting your time trying to remember how to handle arguments or getting lost in the execution log management system
Stay focused on the task you want to accomplish and write beautiful, reliable, and standardized scripts with which you can review detailed execution history, fine-tune execution permissions, manage concurrent executions and more.
Increase maintainability
Increase maintainability and ensure consistency across all scripts within your teams by adhering to Scrippy's formalism.
Scrippy Core Features
Everything an admin script needs, built in
See it in action: a real-life admin task
Every night, the rotated logs of the billing servers must be archived and their disk space freed. Only members of the ops group may run the job, never twice at the same time, and a remote file is deleted only once its archived copy has been verified.
#!/bin/env python3
"""
--------------------------------------------------------------------------------
@author : Harry Fink
@date : 2026-10-07
@version : 1.0.0
@description : Archive rotated application logs and free remote disk space
--------------------------------------------------------------------------------
Update:
1.0.0 2026-10-07 - Harry Fink - cre: Production release
--------------------------------------------------------------------------------
List of authorized users or group:
@group:ops
--------------------------------------------------------------------------------
Concurrent executions:
@max_instance: 1
@exit_on_wait: true
--------------------------------------------------------------------------------
List of mandatory configuration parameters:
@conf:ssh|user|str|false
@conf:ssh|port|int|false
@conf:ssh|key|str|false
@conf:logs|remote_dir|str|false
@conf:logs|pattern|str|false
@conf:archive|dir|str|false
--------------------------------------------------------------------------------
List of execution options and argument parameters:
@args:hosts|str|Servers to collect the logs from|+||||true|false
@args:dry-run|bool|List the files to archive without touching them||false|||false|false
--------------------------------------------------------------------------------
Functioning:
---------------
For each server, rotated log files are downloaded to the archive
directory, verified against their SHA-256 checksum, and only then
deleted from the server.
"""
#-------------------------------------------------------------------------------
# Initialization of the environment
#-------------------------------------------------------------------------------
import os
import shlex
import hashlib
import datetime
import scrippy_core
from scrippy_core import logger
from scrippy_remote.ssh import Ssh
#-------------------------------------------------------------------------------
# Definition of functions and classes
#-------------------------------------------------------------------------------
class ChecksumError(Exception):
pass
def sha256(path):
with open(path, "rb") as archived:
return hashlib.sha256(archived.read()).hexdigest()
def archive_logs(host, config, dry_run):
logger.info(f"[+] Archiving logs from {host}")
remote_dir = config.get("logs", "remote_dir")
archive_dir = os.path.join(config.get("archive", "dir"), host,
datetime.date.today().isoformat())
os.makedirs(archive_dir, exist_ok=True)
with Ssh(username=config.get("ssh", "user"),
host=host,
port=config.get("ssh", "port", "int"),
key=config.get("ssh", "key")) as ssh:
names = ssh.list_remote_dir(remote_dir, pattern=config.get("logs", "pattern"))
logger.info(f" '-> {len(names)} rotated log file(s) found")
for name in names:
remote_file = f"{remote_dir}/{name}"
if dry_run:
logger.info(f" '-> Would archive: {remote_file}")
continue
result = ssh.exec(f"sha256sum {shlex.quote(remote_file)}")
checksum = result["stdout"][0].split()[0]
ssh.get_file(remote_file, archive_dir)
if sha256(os.path.join(archive_dir, name)) != checksum:
raise ChecksumError(f"{host}:{remote_file}")
ssh.delete_remote_file(remote_file)
logger.info(f" '-> Archived: {remote_file}")
#-------------------------------------------------------------------------------
# Main processing
#-------------------------------------------------------------------------------
def main():
with scrippy_core.ScriptContext() as _context:
for host in _context.args.hosts:
archive_logs(host, _context.config, _context.args.dry_run)
#-------------------------------------------------------------------------------
# Entry point
#-------------------------------------------------------------------------------
if __name__ == '__main__':
main()
The script configuration file, exp_archive_logs.conf, is checked against the @conf declarations before anything runs:
[ssh]
user = svc-logs
port = 22
key = /home/harry.fink/.ssh/id_ed25519
[logs]
remote_dir = /var/log/billing
pattern = .*\.log\.\d+\.gz$
[archive]
dir = /srv/archive/billing
Try it first, then let cron run it every night:
exp_archive_logs.py --hosts bill01 bill02 --dry-run
exp_archive_logs.py --hosts bill01 bill02
The next morning, everyone in the team knows what happened:
$ exp_archive_logs.py --hist 3
3 last executions of exp_archive_logs.py
+--------------------------+---------------------+---------------------+--------------+-----------------+-----------------+------+----------------------------------------------+---------------+
| Session | Start | End | Duration | Origin | Exec | Code | Params | Exit |
+--------------------------+---------------------+---------------------+--------------+-----------------+-----------------+------+----------------------------------------------+---------------+
| 1791385201.4127718_48213 | 08/10/2026 02:00:01 | 08/10/2026 02:03:12 | 3 min 11 sec | harry.fink | harry.fink | 0 | ['--hosts', 'bill01', 'bill02'] | 0 |
| 1791298801.2084511_31877 | 07/10/2026 02:00:01 | 07/10/2026 02:01:46 | 1 min 45 sec | harry.fink | harry.fink | 1 | ['--hosts', 'bill01', 'bill02'] | ChecksumError |
| 1791289240.9361205_30154 | 06/10/2026 23:20:40 | 06/10/2026 23:20:42 | 2 sec | luiggi.vercotti | luiggi.vercotti | 0 | ['--hosts', 'bill01', 'bill02', '--dry-run'] | 0 |
+--------------------------+---------------------+---------------------+--------------+-----------------+-----------------+------+----------------------------------------------+---------------+
What went wrong on October 7th? The full log of each execution is one --log <SESSION> away. The remote file was kept, ready for the next run:
$ exp_archive_logs.py --log 1791298801.2084511_31877
[20261007 02:00:01] [INFO ] [+] Opening history (retention=50)
[20261007 02:00:01] [INFO ] [+] Using workspace: /var/tmp/scrippy/exp_archive_logs_1791298801.2084511_31877
[20261007 02:00:01] [INFO ] [+] Archiving logs from bill01
[20261007 02:00:02] [INFO ] '-> 3 rotated log file(s) found
[20261007 02:00:31] [INFO ] '-> Archived: /var/log/billing/billing.log.1.gz
[20261007 02:00:58] [INFO ] '-> Archived: /var/log/billing/billing.log.2.gz
[20261007 02:01:19] [INFO ] '-> Archived: /var/log/billing/billing.log.3.gz
[20261007 02:01:19] [INFO ] [+] Archiving logs from bill02
[20261007 02:01:20] [INFO ] '-> 2 rotated log file(s) found
[20261007 02:01:45] [INFO ] '-> Archived: /var/log/billing/billing.log.1.gz
[20261007 02:01:46] [INFO ] [+] Workspace deletion: /var/tmp/scrippy/exp_archive_logs_1791298801.2084511_31877
[20261007 02:01:46] [INFO ] [+] End: 1 (ChecksumError)
[20261007 02:01:46] [CRITICAL] [ChecksumError]: bill02:/var/log/billing/billing.log.2.gz
Install Scrippy and boost your efficiency
Scrippy installation is no harder than any other Python module and running a command:
pip install scrippy
scrippy install
That's it, you are ready to leap forward!
In the system Python environment
On recent operating systems (Debian 12+, Ubuntu 23.04+...), the system Python environment is externally managed by the operating system (PEP 668). Add the --break-system-packages option to install Scrippy alongside the Python packages provided by your distribution:
pip install --break-system-packages scrippy
scrippy install --break-system-packages
This method is best suited to hosts where you keep control over the system Python packages.
In a virtual environment
A dedicated virtual environment keeps Scrippy and its dependencies isolated from the operating system and works on every system:
python3 -m venv ~/.local/share/scrippy/venv
~/.local/share/scrippy/venv/bin/pip install scrippy
~/.local/share/scrippy/venv/bin/scrippy install
Run your scripts with the ~/.local/share/scrippy/venv/bin/python interpreter. For a system-wide installation, run the same commands as root with /opt/scrippy/venv as the virtual environment.