Skip to content

Notification Templates

Keystone issues automated notifications regarding changes to user accounts and resource allocations. Administrators can customize these notifications using templates to reflect organization-specific branding and messaging.

Overriding Templates

Keystone generates notifications using HTML templates and the Jinja2 templating engine. The location of custom templates is configurable via application settings.

When a notification is triggered, Keystone first checks for a custom template file. If no custom template is available, Keystone falls back to an internal default. The selected template is then rendered using the context data outlined below.

Email notification templates can be rendered and written to disk using the keystone-api render_templates command. It is strongly recommended to develop and test templates locally before deploying them to a production environment. See keystone-api render_templates --help for details.

Template Security

Template files are required to have no write permissions for users other than the owner or group (i.e., o+w). If a template file has world-writeable permissions, Keystone will refuse to load it. This restriction is provided for security and ensures templates cannot be modified by unauthorized users in production.

Notification templates are automatically sanitized to remove any JavaScript or externally loaded resources (e.g. css imports). Users familiar with the Jinja2 templating engine will also find certain Jinja features are not available, including access to application internals and the ability to bypass variable sanitation.

Templates

The following templates are available for customization.

Base Template

Template file: base.html

The base template serves as the parent layout for all notification content, providing top-level styling and structure. The template defines two content blocks that child templates override to inject content.

Available Template Fields
Block Name Description
main Main body content of the email notification.
footer Footer content displayed at the bottom of the message.
Default Template Content
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Keystone User Notification</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    body {
      font-family: Helvetica, sans-serif;
      font-size: 16px;
      line-height: 1.4;
      background-color: #f5f5f5;
      margin: 0;
      padding: 0;
    }

    table.body {
      width: 100%;
    }

    table.main {
      width: 100%;
      background-color: #ffffff;
      border: 1px solid #eaeaea;
      border-radius: 0;
    }

    table.footer {
      width: 100%;
      text-align: center;
      padding-top: 24px;
      padding-bottom: 24px;
    }

    td.container {
      width: 800px;
      padding-top: 24px;
    }

    td.wrapper {
      padding: 24px;
    }

    td.footer-text {
      color: #a1a1a1;
      font-size: 11px;
    }
  </style>
</head>
<body>
<table role="presentation" class="body">
  <tr>
    <td>&nbsp;</td>
    <td class="container">
      <table role="presentation" class="main">
        <tr>
          <td class="wrapper">
            {% block main %}{% endblock %}
          </td>
        </tr>
      </table>
      <table role="presentation" class="footer">
        <tr>
          <td class="footer-text">
            {% block footer %}
              The Keystone HPC Utility <br> &copy; Better HPC
            {% endblock %}
          </td>
        </tr>
      </table>
    </td>
    <td>&nbsp;</td>
  </tr>
</table>
</body>
</html>

Upcoming Resource Expiration

Template file: upcoming_expiration.html

The upcoming expiration notification alerts users that one or more of their active resource allocations is nearing its expiration date.

Available Template Fields
Field Name Type Description
user_name str Username of the notified user.
user_first str First name of the notified user.
user_last str Last name of the notified user.
req_id int ID of the allocation request being notified about.
req_title str Title of the allocation request.
req_team str Name of the team associated with the allocation request.
req_submitted date Date when the allocation request was submitted.
req_active date Date when the allocation request became active.
req_expire date or None Date when the allocation request expires.
req_days_left int or None Number of days remaining until expiration (calculated from current date).
allocations list[dict] List of allocated resources tied to the request. Each item includes:
alloc_cluster str Name of the cluster where the resource is allocated.
alloc_requested int Number of service units requested (or 0 if unavailable).
alloc_awarded int Number of service units awarded (or 0 if unavailable).
upcoming_requests list[dict] List of upcoming or active requests for the same team. Each item includes:
id int ID of the upcoming allocation request.
title str Title of the upcoming allocation request.
submitted date Date when the upcoming request was submitted.
active date Date when the upcoming request became active.
expire date or None Date when the upcoming request expires.
status str Status of the upcoming allocation request.
Default Template Content
{% extends "base.html" %}

{% block main %}
  {% if user_first and user_last %}
    <p>Dear {{ user_first }} {{ user_last }},</p>
  {% else %}
    <p>Dear {{ user_name }},</p>
  {% endif %}

  <p>
    This is a reminder that the <em>{{ req_team }}</em> team's HPC allocation
    <strong>"{{ req_title }}"</strong> (ID: {{ req_id }}) is expiring in
    <strong>{{ req_days_left }} day{{ "s" if req_days_left != 1 }}</strong>,
    on <strong>{{ req_expire.strftime('%B %d, %Y') }}</strong>. After this
    date, any unused service units granted by this allocation will no longer
    be accessible to your team.
  </p>

  <p>
    This expiration only applies to service units awarded under the allocation
    mentioned above. Any service units awarded under different allocations held
    by your team remain unaffected. The table below summarizes the service units
    associated with the expiring allocation:
  </p>

  <table style="width: 100%; border-collapse: collapse; font-family: sans-serif; font-size: 14px;">
    <thead>
    <tr style="background-color: #f2f2f2;">
      <th style="text-align: center; padding: 8px; border: 1px solid #ccc;">Cluster</th>
      <th style="text-align: center; padding: 8px; border: 1px solid #ccc;">Awarded SUs</th>
    </tr>
    </thead>
    <tbody>
    {% for alloc in allocations %}
      <tr>
        <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
          {{ alloc.alloc_cluster }}
        </td>
        <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
          {{ "{:,}".format(alloc.alloc_awarded) }}
        </td>
      </tr>
    {% endfor %}
    </tbody>
  </table>

  <p style="padding-top: 10px;">
    <strong>Recommended Actions</strong>
  </p>

  {% if upcoming_requests %}
    <p>
      The following table includes all active or upcoming allocations currently
      associated with the <em>{{ req_team }}</em> team. Please take a moment to
      review each allocation and confirm your team has sufficient resources moving
      forward. If your team anticipates needing resources beyond those listed
      below, please have a team administrator submit a new allocation request
      at their earliest convenience.
    </p>

    <table
        style="width: 100%; border-collapse: collapse; table-layout: fixed; font-family: sans-serif; font-size: 14px;">
      <thead>
      <tr style="background-color: #f2f2f2;">
        <th style="width: 8%; text-align: center; padding: 8px; border: 1px solid #ccc;">ID</th>
        <th style="width: 30%; text-align: left; padding: 8px; border: 1px solid #ccc;">Title</th>
        <th style="width: 12%; text-align: center; padding: 8px; border: 1px solid #ccc;">Status</th>
        <th style="width: 16%; text-align: center; padding: 8px; border: 1px solid #ccc;">Submitted</th>
        <th style="width: 16%; text-align: center; padding: 8px; border: 1px solid #ccc;">Begins</th>
        <th style="width: 16%; text-align: center; padding: 8px; border: 1px solid #ccc;">Expires</th>
      </tr>
      </thead>
      <tbody>
      {% for req in upcoming_requests %}
        <tr>
          <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
            {{ req.id }}
          </td>
          <td style="text-align: left; padding: 8px; border: 1px solid #ccc; overflow-wrap: break-word; word-break: break-word;">
            {{ req.title }}
          </td>
          <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
            {{ req.status }}
          </td>
          <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
            {{ req.submitted.strftime('%B %d, %Y') if req.submitted else '—' }}
          </td>
          <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
            {{ req.active.strftime('%B %d, %Y') if req.active else '—' }}
          </td>
          <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
            {{ req.expire.strftime('%B %d, %Y') if req.expire else '—' }}
          </td>
        </tr>
      {% endfor %}
      </tbody>
    </table>

  {% else %}
    <p>
      The <em>{{ req_team }}</em> team has no upcoming or active allocations beyond the one described above.
      Once this allocation expires on <strong>{{ req_expire.strftime('%B %d, %Y') }}</strong>, your team will have zero
      remaining service units. If your team requires additional service units, please have a team administrator submit
      a new allocation request at their earliest convenience.
    </p>

    <p>
      If your team does not require additional service units, then no action is required at this time.
    </p>
  {% endif %}

  <p style="padding-top: 10px;">
    Best regards,<br>The Keystone HPC Utility
  </p>
{% endblock %}

Expired Resource Allocation

Template file: past_expiration.html

The past expiration notification alerts users that one or more of their active resource allocations has expired and that the resources granted under that allocation are no longer available for use.

Available Template Fields
Field Name Type Description
user_name str Username of the notified user.
user_first str First name of the notified user.
user_last str Last name of the notified user.
req_id int ID of the allocation request being notified about.
req_title str Title of the allocation request.
req_team str Name of the team associated with the allocation request.
req_submitted date Date when the allocation request was submitted.
req_active date Date when the allocation request became active.
req_expire date or None Date when the allocation request expires.
allocations list[dict] List of allocated resources tied to the request. Each item includes:
alloc_cluster str Name of the cluster where the resource is allocated.
alloc_requested int Number of service units requested (or 0 if unavailable).
alloc_awarded int Number of service units awarded (or 0 if unavailable).
alloc_final int Number of service units used by the team (or 0 if unavailable).
upcoming_requests list[dict] List of upcoming or active requests for the same team. Each item includes:
id int ID of the upcoming allocation request.
title str Title of the upcoming allocation request.
submitted date Date when the upcoming request was submitted.
active date Date when the upcoming request became active.
expire date or None Date when the upcoming request expires.
status str Status of the upcoming allocation request.
Default Template Content
{% extends "base.html" %}

{% macro percent(numerator, denominator) %}
  {% if denominator %}
    ({{ '%.0f' % ((numerator / denominator) * 100) }}%)
  {% else %}
    &nbsp;
  {% endif %}
{% endmacro %}

{% block main %}
  {% if user_first and user_last %}
    <p>Dear {{ user_first }} {{ user_last }},</p>
  {% else %}
    <p>Dear {{ user_name }},</p>
  {% endif %}

  <p>
    This is a reminder that the <em>{{ req_team }}</em> team's HPC allocation
    <strong>"{{ req_title }}"</strong> (ID: {{ req_id }}) expired on
    <strong>{{ req_expire.strftime('%B %d, %Y') }}</strong>. Moving forward,
    any unused service units granted by this allocation are no longer usable
    by your team.
  </p>

  <p>
    The table below summarizes your team's total resource usage under this allocation:
  </p>

  <table style="width: 100%; border-collapse: collapse; font-family: sans-serif; font-size: 14px;">
    <thead>
    <tr style="background-color: #f2f2f2;">
      <th style="text-align: center; padding: 8px; border: 1px solid #ccc;">Cluster</th>
      <th style="text-align: center; padding: 8px; border: 1px solid #ccc;">Awarded SUs</th>
      <th style="text-align: center; padding: 8px; border: 1px solid #ccc;">Utilized SUs</th>
    </tr>
    </thead>
    <tbody>
    {% for alloc in allocations %}
      <tr>
        <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
          {{ alloc.alloc_cluster }}
        </td>
        <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
          {{ "{:,}".format(alloc.alloc_awarded) }}
        </td>
        <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
          {{ "{:,}".format(alloc.alloc_final) }} {{ percent(alloc.alloc_final, alloc.alloc_awarded) }}
        </td>
      </tr>
    {% endfor %}
    </tbody>
  </table>

  <p style="padding-top: 10px">
    <strong>What Does This Mean</strong>
  </p>

  <p>
    On {{ req_active.strftime('%B %d, %Y') }} your team was granted the
    service unit allocation listed above. This allocation expired on
    {{ req_expire.strftime('%B %d, %Y') }} and any unused service units
    are no longer available for use.
  </p>

  <p>
    Any resources granted to your team by other allocation requests remain
    unaffected. The system will automatically apply as much of your system
    usage as possible to the expiring request, maximizing the total service
    units available in any remaining allocations.
  </p>

  <p style="padding-top: 10px;">
    <strong>Recommended Actions</strong>
  </p>

  {% if upcoming_requests %}
    <p>
      The following table includes all active or upcoming allocations currently
      associated with the <em>{{ req_team }}</em> team. Please take a moment to
      review each allocation and confirm your team has sufficient resources moving
      forward. If your team anticipates needing resources beyond those listed
      below, please have a team administrator submit a new allocation request
      at their earliest convenience.
    </p>

    <table
        style="width: 100%; border-collapse: collapse; table-layout: fixed; font-family: sans-serif; font-size: 14px;">
      <thead>
      <tr style="background-color: #f2f2f2;">
        <th style="width: 8%; text-align: center; padding: 8px; border: 1px solid #ccc;">ID</th>
        <th style="width: 30%; text-align: left; padding: 8px; border: 1px solid #ccc;">Title</th>
        <th style="width: 12%; text-align: center; padding: 8px; border: 1px solid #ccc;">Status</th>
        <th style="width: 16%; text-align: center; padding: 8px; border: 1px solid #ccc;">Submitted</th>
        <th style="width: 16%; text-align: center; padding: 8px; border: 1px solid #ccc;">Begins</th>
        <th style="width: 16%; text-align: center; padding: 8px; border: 1px solid #ccc;">Expires</th>
      </tr>
      </thead>
      <tbody>
      {% for req in upcoming_requests %}
        <tr>
          <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
            {{ req.id }}
          </td>
          <td style="text-align: left; padding: 8px; border: 1px solid #ccc; overflow-wrap: break-word; word-break: break-word;">
            {{ req.title }}
          </td>
          <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
            {{ req.status }}
          </td>
          <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
            {{ req.submitted.strftime('%B %d, %Y') if req.submitted else '—' }}
          </td>
          <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
            {{ req.active.strftime('%B %d, %Y') if req.active else '—' }}
          </td>
          <td style="text-align: center; padding: 8px; border: 1px solid #ccc;">
            {{ req.expire.strftime('%B %d, %Y') if req.expire else '—' }}
          </td>
        </tr>
      {% endfor %}
      </tbody>
    </table>

  {% else %}
    <p>
      The <em>{{ req_team }}</em> team has no upcoming or active allocations.
      If your team requires additional service units, please have a team administrator submit a new allocation request
      at their earliest convenience.
      If your team does not require additional service units, then no action is required at this time.
    </p>
  {% endif %}

  <p style="padding-top: 10px;">
    Best regards,<br>The Keystone HPC Utility
  </p>
{% endblock %}