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 %}