4. Configuration file

Hard coded values make a script hard to maintain and impossible to reuse with other parameters. In this step, the mount points and the thresholds of disk_watch.py move to a configuration file, automatically loaded and validated by Scrippy.

The configuration file

Each Scrippy script may have its own configuration file:

  • located in the confdir directory,
  • named after the script, with the .conf extension instead of .py.

Create the file /home/luiggi/.local/share/scrippy/conf/disk_watch.conf:

[log]
level = info

[filesystems]
mount_points = /, /home, /var

[thresholds]
warning = 80
critical = 90

The configuration file is an INI file, divided into as many sections as needed:

  • Indentation is possible but optional.
  • A line beginning with # or ; is a comment, even if it is indented.
  • Spaces are accepted in section names, key names and values.
  • All values are strings: The script converts them to the expected type when reading them (see Reading the configuration).

Warning: Comments at the end of a line are not supported: In mount_points = /, /home # main disks, the comment is part of the value.

Warning: The % character is used for value interpolation (i.e: %(key)s is replaced by the value of key from the same section). A literal % must be doubled: password = 50%%off is read as 50%off. A single % makes the script fail with an InterpolationSyntaxError.

The code

#!/usr/bin/env python3
"""
--------------------------------------------------------------------------------
  @author         : Luiggi Vercotti
  @date           : 2026-10-08
  @version        : 0.4.0
  @description    : Monitor filesystem usage

--------------------------------------------------------------------------------
  Update:
  0.1.0  2026-10-08 - Luiggi Vercotti - cre: Script creation
  0.2.0  2026-10-08 - Luiggi Vercotti - evo: Several mount points, log levels
  0.3.0  2026-10-08 - Luiggi Vercotti - evo: Monitoring exit status
  0.4.0  2026-10-08 - Luiggi Vercotti - evo: Configuration file

--------------------------------------------------------------------------------
  List of mandatory configuration parameters:
    @conf:filesystems|mount_points|str|false
    @conf:thresholds|warning|int|false
    @conf:thresholds|critical|int|false

--------------------------------------------------------------------------------
  Functioning:
  ---------------
    Checks the usage of each mount point against a warning and a critical
    threshold and exits with a monitoring compatible exit status:
      0: OK, 1: WARNING, 2: CRITICAL

    Configuration:
      - filesystems::mount_points: Comma separated list of mount points
      - thresholds::warning: Warning threshold in percent
      - thresholds::critical: Critical threshold in percent
"""
# ------------------------------------------------------------------------------
#  Initialization of the environment
# ------------------------------------------------------------------------------
import sys
import shutil
import scrippy_core
from scrippy_core import logger

OK, WARNING, CRITICAL = 0, 1, 2
STATUS = {OK: "OK", WARNING: "WARNING", CRITICAL: "CRITICAL"}

# ------------------------------------------------------------------------------
#  Definition of functions and classes
# ------------------------------------------------------------------------------
def get_mount_points(config):
  return [mp.strip() for mp in config.get("filesystems", "mount_points").split(",")]

def get_usage(mount_point):
  logger.debug(f"[+] Checking {mount_point}")
  usage = shutil.disk_usage(mount_point)
  return round(usage.used * 100 / usage.total, 1)

def check(mount_points, warning, critical):
  worst = OK
  for mount_point in mount_points:
    usage = get_usage(mount_point)
    if usage >= critical:
      logger.error(f" '-> {mount_point}: {usage}% [CRITICAL]")
      status = CRITICAL
    elif usage >= warning:
      logger.warning(f" '-> {mount_point}: {usage}% [WARNING]")
      status = WARNING
    else:
      logger.info(f" '-> {mount_point}: {usage}% [OK]")
      status = OK
    worst = max(worst, status)
  return worst

# ------------------------------------------------------------------------------
#  Main processing
# ------------------------------------------------------------------------------
def main():
  with scrippy_core.ScriptContext(retention=100) as _context:
    config = _context.config
    warning = config.get("thresholds", "warning", "int")
    critical = config.get("thresholds", "critical", "int")
    logger.log_section("disk_watch", f"Thresholds: warning={warning}% critical={critical}%")
    status = check(get_mount_points(config), warning, critical)
    logger.info(f"[+] Status: {STATUS[status]}")
    if status != OK:
      sys.exit(status)

# ------------------------------------------------------------------------------
#  Entry point
# ------------------------------------------------------------------------------
if __name__ == '__main__':
  main()

What changed:

  • The MOUNT_POINTS, WARNING_THRESHOLD and CRITICAL_THRESHOLD constants are gone.
  • The header declares the expected configuration with @conf lines.
  • main() reads the thresholds from the configuration, and get_mount_points() reads the mount points.
  • The Functioning section documents the configuration.
./disk_watch.py
[20261008 19:10:55] [INFO    ] [+] Secrets registration (args)
[20261008 19:10:55] [INFO    ] [+] Loading configuration file /home/luiggi/.local/share/scrippy/conf/disk_watch.conf
[20261008 19:10:55] [INFO    ] [+] Configuration check
[20261008 19:10:55] [INFO    ] [+] Secrets registration (conf)
[20261008 19:10:55] [INFO    ] [+] Opening history (retention=100)
[...]

Configuration validation

The @conf lines of the header describe the configuration the script needs:

@conf:<section>|<key>|<type>|<secret>
Field Content
section The section of the configuration file
key The key within the section
type The type of the value: str, int, float or bool
secret true if the value must be redacted from the logs, false otherwise (see Secrets)

All fields are mandatory and can not be empty.

When entering the execution context, Scrippy checks that each declared key exists and that its value matches the declared type. If not, the script stops immediately, before running any of your code:

[20261008 19:10:55] [INFO    ] [+] Configuration check
[20261008 19:10:55] [CRITICAL] [ScrippyCoreError]: Type error: [thresholds][warning] is not of type 'int'
[20261008 19:10:55] [INFO    ] [+] No configuration file /home/luiggi/.local/share/scrippy/conf/disk_watch.conf
[20261008 19:10:55] [INFO    ] [+] Configuration check
[20261008 19:10:55] [CRITICAL] [ScrippyCoreError]: Missing or unknown configuration section: [filesystems]

A wrong configuration is detected at startup, not in the middle of the processing after some actions have already been done.

Only the structure and the types are checked, not the values themselves: A negative threshold is a valid int. Checking the values is up to the script, as you will see in the Error handling step.

An invalid @conf declaration (missing or extra field, unknown type...) is also reported at startup:

[CRITICAL] [ScrippyCoreError]: Invalid @conf declaration(s):
  - @conf:thresholds|critical|integer: 3 fields found, 4 expected (<section>|<key>|<value_type>|<secret>)

With the --debug option, each declared key is displayed during the check:

[20261008 19:10:55] [DEBUG   ] [filesystems][mount_points] = /, /home, /var [OK]
[20261008 19:10:55] [DEBUG   ] [thresholds][warning] = 80 [OK]
[20261008 19:10:55] [DEBUG   ] [thresholds][critical] = 90 [OK]

Note: The configuration file is optional: A script without @conf declarations runs without configuration file. Sections and keys that are present in the file but not declared are not checked, but remain available to the script.

Reading the configuration

The configuration is available through _context.config.

get()

config.get(section, key, param_type="str")

Returns the value of key in section, converted to param_type: str (default), int, float or bool.

mount_points = config.get("filesystems", "mount_points")    # '/, /home, /var'
warning = config.get("thresholds", "warning", "int")        # 80

For the bool type, 1, yes, true and on are true, 0, no, false and off are false (case insensitive).

A missing section or key, or a value that does not match the requested type, raises a ScrippyCoreError.

has()

config.has(section, key)

Returns True if key exists in section. Use it to read optional, undeclared keys:

if config.has("filesystems", "exclude"):
  exclude = config.get("filesystems", "exclude")

get_section()

config.get_section(section)

Returns all the keys of a section as a dictionary of strings:

config.get_section("thresholds")    # {'warning': '80', 'critical': '90'}

A missing section raises a ScrippyCoreError.

The [log] section

The level key of the [log] section is reserved: When present, Scrippy applies it as the log level of the script when entering the execution context. It does not need to be declared with @conf.

[log]
level = warning

The value is one of debug, info, warning, error or critical (case insensitive). An invalid value stops the script:

[CRITICAL] [ScrippyCoreError]: Invalid log level 'verbose' (expected: debug, info, warning, error, critical)

With level = warning, a run where all mount points are OK displays nothing after the configuration check. The --debug option has priority over the configuration file:

[20261008 19:10:55] [DEBUG   ] [+] Log level is forced: Ignoring log level change to info

What's next

Changing a threshold for a single run requires to edit the configuration file. In the next step, command line options will allow to override the configuration.