How to Pass SaltStack State Variables to Jinja Templates for Dynamic OpenVPN Config Generation


2 views

When automating OpenVPN configuration management with SaltStack, we often need to generate multiple client-specific configuration files based on pillar data. The challenge arises when we need to access the loop variable (vpnuser) from the state file within the Jinja template.

{% for vpnuser, userdata in salt['pillar.get']('openvpn', {}).items() %}
/etc/openvpn/ccd/{{ vpnuser }}:
  file.managed:
    - template: jinja
    - source: salt://openvpn/ccd_template.j2
    - context:
        current_user: {{ vpnuser }}
        user_config: {{ userdata|json }}
{% endfor %}

Here's how to utilize the passed variables in your Jinja template (ccd_template.j2):

# OpenVPN Client Config for {{ current_user }}
# IP Address: {{ user_config.ip }}

{% for config_line in user_config.config %}
{{ config_line }}
{% endfor %}

For more complex scenarios, you can pass the entire pillar subtree:

    - context:
        vpn: {{ salt['pillar.get']('openvpn:' ~ vpnuser)|json }}
        username: {{ vpnuser }}

Complete implementation with error handling:

# In your state file
{% set vpn_users = salt['pillar.get']('openvpn', {}) %}

{% for user, config in vpn_users.items() if config %}
/etc/openvpn/ccd/{{ user }}:
  file.managed:
    - template: jinja
    - source: salt://openvpn/client_config.j2
    - context:
        user: {{ user }}
        ip: {{ config.get('ip', '') }}
        custom_settings: {{ config.get('config', [])|json }}
    - defaults:
        route: 192.168.1.0/24
{% endfor %}

When dealing with hundreds of VPN users:

  • Use json filter to safely pass complex data structures
  • Consider batching template rendering for large deployments
  • Cache pillar data when possible

When working with SaltStack's configuration management, a common requirement is generating multiple configuration files dynamically based on pillar data. The specific challenge lies in passing state-level Jinja variables into template files while maintaining clean state declarations.

Consider this pillar data structure for OpenVPN client configurations:

openvpn:
  user1:
    ip: 1.2.3.4
    config:
      - push "route 10.0.0.0 255.255.255.0"
      - push "dhcp-option DNS 8.8.8.8"
  user2:
    ip: 5.6.7.8
    config:
      - push "route 192.168.1.0 255.255.255.0"

The most effective method uses SaltStack's context passing mechanism. Here's the complete implementation:

State File Implementation

{% for username, userdata in salt['pillar.get']('openvpn', {}).items() %}
/etc/openvpn/ccd/{{ username }}:
  file.managed:
    - template: jinja
    - source: salt://openvpn/ccd.j2
    - context:
        user_ip: {{ userdata.ip }}
        user_config: {{ userdata.config }}
{% endfor %}

Template File (ccd.j2)

# Client-specific configuration for {{ id }}
ifconfig-push {{ user_ip }} 255.255.255.0
{% for config_line in user_config %}
{{ config_line }}
{% endfor %}

For more complex scenarios, you can pass the entire user namespace:

{% for username, userdata in salt['pillar.get']('openvpn', {}).items() %}
/etc/openvpn/ccd/{{ username }}:
  file.managed:
    - template: jinja
    - source: salt://openvpn/ccd.j2
    - context:
        user: {{ userdata|json }}
{% endfor %}

Then in the template:

{# Access all user data via the user namespace #}
ifconfig-push {{ user.ip }} 255.255.255.0
{% for config_line in user.config %}
{{ config_line }}
{% endfor %}

Always include error checking in your templates:

{# ccd.j2 with error checking #}
{% if user is defined and user.ip is defined %}
ifconfig-push {{ user.ip }} 255.255.255.0
{% else %}
{# Log error or use default IP #}
ifconfig-push 10.8.0.100 255.255.255.0
{% endif %}