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> </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> © Better HPC
{% endblock %}
</td>
</tr>
</table>
</td>
<td> </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 %}
{% 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 %}