Skip to content

Flexible configuration syntax #107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
*.egg-info/
*.py[co]
*.swp
.cache/
.coverage
.kitchen
.tox/
__pycache__/
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
sudo: false
language: python
python:
- '2.7'
- '3.5'
- '3.6'
install: pip install tox-travis
script: tox
196 changes: 175 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
haproxy
========
=======

Installs and configures [HAProxy 1.5](http://www.haproxy.org/).

@@ -13,6 +13,7 @@ The **last release** in the 1.x series is [1.2.0](https://github.com/devops-coop
Features
--------

* Offers flexible, structured configuration syntax.
* Supports Alpine, CentOS, Debian, and Ubuntu.
* Installs HAProxy 1.5 from official repositories on Debian and Ubuntu.
* Installs EPEL repository on CentOS.
@@ -26,40 +27,193 @@ Role Variables
* `haproxy_defaults`

Default settings for frontends, backends, and listen proxies.
* `haproxy_mailers`

A map of [`mailers` configurations](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#3.6) sections.
* `haproxy_peers`

A map of [`peer` configurations](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#3.5).
* `haproxy_resolvers`

A list of HAProxy resolvers.
A map of [`resolvers` configurations](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#5.3.2).
* `haproxy_userlists`

A map of [`userlist` configurations](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#3.4).
* `haproxy_backends`

A list of HAProxy backends.
A map of [`backend` configurations](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4).
* `haproxy_frontends`

A list of HAProxy frontends.
A map of [`frontend` configurations](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4).
* `haproxy_listen`

A list of listen proxies.
A map of [`listen` configurations](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4).

Configuration
-------------

HAProxy is a complex piece of software and supports hundreds of configuration options.

Rather than support individual options, this role offers a flexible, structured configuration syntax for HAProxy concepts.

For example, instead of declaring `errorfile` lines like this:

```yaml
haproxy_defaults:
errorfile:
- code: 400
file: /etc/haproxy/errors/400.http
- code: 403
file: /etc/haproxy/errors/403.http
```

You would use the following natural syntax:

```yaml
haproxy_defaults:
errorfile:
400: /etc/haproxy/errors/400.http
403: /etc/haproxy/errors/403.http
```

The descriptions below detail how the role handles different data types.

### Maps

The template expands maps (dictionaries, hashes) according to three simple rules:

1. Prefix every value in the map with the key name.
2. Sort option names by relative weight. This ensures HAProxy processes `acl` rules before `http-request` rules, for example.
3. Within each weighted group, sort option names alphabetically.

This ensures predictable output and eliminates runtime warnings.

```yaml
---
haproxy_defaults:
timeout:
server: 500ms
connect: 1000s
client: 5000s
retries: 5
option:
- forwardfor
- dontlognull
```

```
defaults
option forwardfor
option dontlognull
retries 5
timeout client 5000s
timeout connect 1000s
timeout server 500ms
```

See [`vars/main.yml`](vars/main.yml) for a complete list of configurable .
### Sequences

The template will preserve sequence (list, array) order. This is important in some contexts, such as `http-request` rules.

```yaml
---
haproxy_frontends:
http-in:
acl:
- is_admin path_beg /admin
- is_api path_beg /api
default_backend: http
```

```
frontend http-in
acl is_admin path_beg /admin
acl is_api path_beg /api
default_backend http
```

### Booleans

True values (`true`, `yes`, `y`, etc.) will expand to simple flags:

```yaml
---
haproxy_backends:
http:
disabled: true
server:
www: 192.0.2.1:80
```

```
backend http
disabled
server www 192.0.2.1:80
```

False values will omit the flag:

```yaml
---
haproxy_backends:
http:
disabled: false
server:
www: 192.0.2.1:80
```

```
backend http
server www 192.0.2.1:80
```

### Strings, integers, floats, and other primitives

Other primitives will be rendered as-is:

```yaml
haproxy_listens:
tcp-in:
backlog: 10000
maxconn: 1000
mode: tcp
```

```
listen tcp-in
backlog 10000
maxconn 1000
mode tcp
```

Example
-------

```yaml
- hosts: loadbalancers
roles:
- role: haproxy
haproxy_frontends:
- name: 'fe-mysupersite'
ip: '123.123.123.120'
port: '80'
maxconn: '1000'
default_backend: 'be-mysupersite'
haproxy_backends:
- name: 'be-mysupersite'
description: 'mysupersite is really cool'
servers:
- name: 'be-mysupersite-01'
ip: '192.168.1.100'
---
haproxy_frontends:
http:
bind:
- :80
- :443 ssl crt /etc/ssl/certs/star.example.org.pem
acl:
- is_api hdr(Host) api.example.org
use_backend:
- api if is_api
redirect:
- scheme https if { !ssl_fc }
default_backend: app

haproxy_backends:
app:
server:
app-01: 192.51.100.10:80 check
app-02: 192.51.100.11:80 check

api:
server:
api-01: 192.51.100.100:80 check
api-02: 192.51.100.101:80 check
```

License
63 changes: 21 additions & 42 deletions defaults/main.yml
Original file line number Diff line number Diff line change
@@ -14,61 +14,40 @@ _haproxy_ssl_ciphers: 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305

haproxy_global:
log:
- address: /dev/log
facility: local0
- address: /dev/log
facility: local1
level: notice
/dev/log:
- local0
- local1 notice
chroot: /var/lib/haproxy
user: haproxy
group: haproxy
daemon: true
ssl_default_bind_options: '{{ _haproxy_ssl_options }}'
ssl_default_bind_ciphers: '{{ _haproxy_ssl_ciphers }}'
ssl_default_server_options: '{{ _haproxy_ssl_options }}'
ssl_default_server_ciphers: '{{ _haproxy_ssl_ciphers }}'
tune:
ssl:
default-dh-param: 2048
ssl-default-bind-options: '{{ _haproxy_ssl_options }}'
ssl-default-bind-ciphers: '{{ _haproxy_ssl_ciphers }}'
ssl-default-server-options: '{{ _haproxy_ssl_options }}'
ssl-default-server-ciphers: '{{ _haproxy_ssl_ciphers }}'
tune.ssl.default-dh-param: 2048

haproxy_defaults:
mode: http
log:
- address: /dev/log
facility: local1
level: notice
/dev/log:
- local1 notice
timeout:
- param: 'connect'
value: '5000ms'
- param: 'client'
value: '50000ms'
- param: 'server'
value: '50000ms'
options:
connect: 5000ms
client: 50000ms
server: 50000ms
option:
- httpclose
- forwardfor except 127.0.0.0/8
- redispatch
- abortonclose
- httplog
- dontlognull
errorfile:
- code: 400
file: /etc/haproxy/errors/400.http
- code: 403
file: /etc/haproxy/errors/403.http
- code: 408
file: /etc/haproxy/errors/408.http
- code: 500
file: /etc/haproxy/errors/500.http
- code: 502
file: /etc/haproxy/errors/502.http
- code: 503
file: /etc/haproxy/errors/503.http
- code: 504
file: /etc/haproxy/errors/504.http

haproxy_resolvers: []
haproxy_backends: []
haproxy_frontends: []
haproxy_listen: []
haproxy_userlists: []
400: /etc/haproxy/errors/400.http
403: /etc/haproxy/errors/403.http
408: /etc/haproxy/errors/408.http
500: /etc/haproxy/errors/500.http
502: /etc/haproxy/errors/502.http
503: /etc/haproxy/errors/503.http
504: /etc/haproxy/errors/504.http
Empty file added filter_plugins/__init__.py
Empty file.
91 changes: 91 additions & 0 deletions filter_plugins/haproxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from __future__ import unicode_literals

import collections
import sys


if sys.version_info.major < 3:
string_types = basestring
else:
string_types = str


# Certain options need to precede others in the configuration file. Options with
# larger weights sink to the bottom.
# Weights taken from Puppetlabs HAProxy module:
# <https://github.com/puppetlabs/puppetlabs-haproxy/blob/093b9ec1c56551a9e9679dbb3eeef87da851737e/templates/fragments/_options.erb>
OPTION_WEIGHTS = {
'acl': -1,
'tcp-request': 2,
'block': 3,
'http-request': 4,
'reqallow': 5,
'reqdel': 5,
'reqdeny': 5,
'reqidel': 5,
'reqideny': 5,
'reqipass': 5,
'reqirep': 5,
'reqitarpit': 5,
'reqpass': 5,
'reqrep': 5,
'reqtarpit': 5,
'reqadd': 6,
'redirect': 7,
'use_backend': 8,
'use-server': 9,
'server': 100,
}


def sort_by_weight(mapping, weights=None):
"""
Sort mapping keys first by weight, then alphabetically.
"""
if weights:
key = lambda kv: (weights.get(kv[0], 0), kv[0])
else:
key = None
return collections.OrderedDict(sorted(mapping.items(), key=key))


def expand(options):
"""
Expand a nested configuration dictionary.
"""
for key, value in sort_by_weight(options, weights=OPTION_WEIGHTS).items():
if not isinstance(value, collections.Container) or isinstance(value, string_types):
yield (key, value)
continue
elif isinstance(value, collections.Sequence):
for item in value:
yield (key, item)
continue
elif isinstance(value, collections.Mapping):
yield (key, list(expand((value))))
continue
yield (key, value)


def to_haproxy(options):
"""
Yield HAProxy configuration lines from a nested configuration dictionary.
"""
options = expand(options)
for key, value in options:
if value is True:
yield str(key)
elif value is False:
continue
elif isinstance(value, collections.Sequence) and not isinstance(value, string_types):
for sub_key, sub_value in value:
yield '{} {} {}'.format(key, sub_key, sub_value)
else:
yield '{} {}'.format(key, value)


class FilterModule(object):
def filters(self):
return {
'to_haproxy': to_haproxy,
}
10 changes: 10 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from setuptools import (
find_packages,
setup,
)


setup(
name='ansible-haproxy',
packages=find_packages(),
)
198 changes: 3 additions & 195 deletions tasks/configure.yml
Original file line number Diff line number Diff line change
@@ -1,199 +1,7 @@
---

## ASSEMBLE CONFIG - RESOLVERS

- name: 'Create directory for the resolvers'
file:
path: "{{ haproxy_config_dir }}/resolvers.d"
state: directory

- name: "List files for the resolvers"
find:
paths: "{{ haproxy_config_dir }}/resolvers.d"
patterns: "*.cfg"
register: directory_contents
changed_when: false

- name: "Remove unmanaged files for the resolvers"
file:
path: "{{ item.path }}"
state: absent
when: (item.path | basename) not in (haproxy_resolvers | json_query('[*].name') | map('regex_replace', '(.*)', '\\1.cfg') | list)
with_items: "{{ directory_contents.files }}"

- name: 'Build up the resolvers'
- name: Configure HAProxy
template:
src: "resolvers.cfg"
dest: "{{ haproxy_config_dir }}/resolvers.d/{{ item.name }}.cfg"
with_items: "{{ haproxy_resolvers }}"
when: haproxy_resolvers is defined

## ASSEMBLE CONFIG - FRONTEND

- name: 'Create directory for the frontend'
file:
path: "{{ haproxy_config_dir }}/frontends.d"
state: directory

- name: "List files for the frontends"
find:
paths: "{{ haproxy_config_dir }}/frontends.d"
patterns: "*.cfg"
register: directory_contents
changed_when: false

- name: "Remove unmanaged files for the frontends"
file:
path: "{{ item.path }}"
state: absent
when: (item.path | basename) not in (haproxy_frontends | json_query('[*].name') | map('regex_replace', '(.*)', '\\1.cfg') | list)
with_items: "{{ directory_contents.files }}"

- name: 'Build up the frontends'
template:
src: "frontend.cfg"
dest: "{{ haproxy_config_dir }}/frontends.d/{{ item.name }}.cfg"
with_items: "{{ haproxy_frontends }}"
when: haproxy_frontends is defined

## ASSEMBLE CONFIG - BACKEND

- name: 'Create directory for the backends'
file:
path: "{{ haproxy_config_dir }}/backends.d"
state: directory

- name: "List files for the backends"
find:
paths: "{{ haproxy_config_dir }}/backends.d"
patterns: "*.cfg"
register: directory_contents
changed_when: false

- name: "Remove unmanaged files for the backends"
file:
path: "{{ item.path }}"
state: absent
when: (item.path | basename) not in (haproxy_backends | json_query('[*].name') | map('regex_replace', '(.*)', '\\1.cfg') | list)
with_items: "{{ directory_contents.files }}"

- name: 'Build up the backends'
template:
src: "backend.cfg"
dest: "{{ haproxy_config_dir }}/backends.d/{{ item.name }}.cfg"
with_items: "{{ haproxy_backends }}"
when: haproxy_backends is defined

## ASSEMBLE CONFIG - LISTEN

- name: 'Create directory for the listen sections'
file:
path: "{{ haproxy_config_dir }}/listen.d"
state: directory

- name: "List files the listen sections"
find:
paths: "{{ haproxy_config_dir }}/listen.d"
patterns: "*.cfg"
register: directory_contents
changed_when: false

- name: "Remove unmanaged files the listen sections"
file:
path: "{{ item.path }}"
state: absent
when: (item.path | basename) not in (haproxy_listen | json_query('[*].name') | map('regex_replace', '(.*)', '\\1.cfg') | list)
with_items: "{{ directory_contents.files }}"

- name: 'Build up the listen sections'
template:
src: "listen.cfg"
dest: "{{ haproxy_config_dir }}/listen.d/{{ item.name }}.cfg"
with_items: "{{ haproxy_listen }}"
when: haproxy_listen is defined

## ASSEMBLE CONFIG - USERLIST

- name: 'Create directory for the userlists'
file: path={{ haproxy_config_dir }}/userlists.d state=directory

- name: "List files for the userlists"
find:
paths: "{{ haproxy_config_dir }}/userlists.d"
patterns: "*.cfg"
register: directory_contents
changed_when: false

- name: "Remove unmanaged files for the userlists"
file:
path: "{{ item.path }}"
state: absent
when: (item.path | basename) not in (haproxy_userlists | json_query('[*].name') | map('regex_replace', '(.*)', '\\1.cfg') | list)
with_items: "{{ directory_contents.files }}"

- name: 'Build up the userlist sections'
template:
src: userlist.cfg
dest: "{{ haproxy_config_dir }}/userlists.d/{{ item.name }}.cfg"
with_items: "{{ haproxy_userlists }}"
when: haproxy_userlists is defined

## ASSEMBLE CONFIG - GLOBAL & DEFAULT

- name: 'Delete the compiled folder'
file:
path: "{{ haproxy_config_dir }}/compiled"
state: absent

- name: 'Create the compiled folder'
file:
path: "{{ haproxy_config_dir }}/compiled"
state: directory

- name: 'Build up the global config'
template:
src: "global.cfg"
dest: "{{ haproxy_config_dir }}/compiled/01-global.cfg"
when: haproxy_global is defined
tags: 'test'

- name: 'Build up the default config'
template:
src: "defaults.cfg"
dest: "{{ haproxy_config_dir }}/compiled/02-defaults.cfg"
when: haproxy_defaults is defined

## ASSEMBLE FINAL CONFIG

- name: 'Assemble the resolvers sections configuration file'
assemble:
src: "{{ haproxy_config_dir }}/resolvers.d"
dest: "{{ haproxy_config_dir }}/compiled/03-resolvers.cfg"

- name: 'Assemble the backends configuration file'
assemble:
src: "{{ haproxy_config_dir }}/backends.d"
dest: "{{ haproxy_config_dir }}/compiled/04-backends.cfg"

- name: 'Assemble the frontends configuration file'
assemble:
src: "{{ haproxy_config_dir }}/frontends.d"
dest: "{{ haproxy_config_dir }}/compiled/05-frontends.cfg"

- name: 'Assemble the listen sections configuration file'
assemble:
src: "{{ haproxy_config_dir }}/listen.d"
dest: "{{ haproxy_config_dir }}/compiled/06-listen.cfg"

- name: 'Assemble the userlists sections configuration file'
assemble:
src: "{{ haproxy_config_dir }}/userlists.d"
dest: "{{ haproxy_config_dir }}/compiled/07-userlists.cfg"

- name: 'Assemble the final configuration file'
assemble:
src: "{{ haproxy_config_dir }}/compiled"
src: haproxy.cfg.j2
dest: "{{ haproxy_config_file }}"
backup: yes
validate: "haproxy -c -f %s"
validate: haproxy -c -f %s
notify: restart haproxy
24 changes: 14 additions & 10 deletions templates/_macros.j2
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
{%- macro http_response(responses = []) -%}
{%- for response in responses -%}
http-response {{ response.action }}{% if response.param is defined %} {{ response.param }}{% endif %}{% if response.condition is defined %} {{ response.condition }}{% endif %}
{% macro section(section, instances, single=False) %}
{% if instances is defined %}
{% if single %}
{{ section }}
{{ instances|to_haproxy|join('\n')|indent(indentfirst=True) }}

{% endfor -%}
{%- endmacro -%}

{%- macro http_request(requests = []) -%}
{%- for request in requests -%}
http-request {{ request.action }}{% if request.param is defined %} {{ request.param }}{% endif %}{% if request.condition is defined %} {{ request.condition }}{% endif %}
{% else %}
{% for name, options in (instances|dictsort(case_sensitive=True)).items() %}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'items()' will produce error: AnsibleUndefinedVariable: 'list object' has no attribute 'items'
Vars:

haproxy_frontends:
  rgw_http:
    bind:
      - ':80'
    reqadd:
      - 'X-Forwarded-Proto:\ http'
    default_backend: rgw

{{ section }} {{ name }}
{{ options|to_haproxy|join('\n')|indent(indentfirst=True) }}

{% endfor -%}
{%- endmacro -%}
{% endfor %}

{% endif %}
{% endif %}
{% endmacro %}
134 changes: 0 additions & 134 deletions templates/backend.cfg

This file was deleted.

88 changes: 0 additions & 88 deletions templates/defaults.cfg

This file was deleted.

134 changes: 0 additions & 134 deletions templates/frontend.cfg

This file was deleted.

64 changes: 0 additions & 64 deletions templates/global.cfg

This file was deleted.

13 changes: 13 additions & 0 deletions templates/haproxy.cfg.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{%- import '_macros.j2' as macros with context -%}
# {{ ansible_managed }}


{{ macros.section('global', haproxy_global, single=True) -}}
{{- macros.section('defaults', haproxy_defaults, single=True) -}}
{{- macros.section('peers', haproxy_peers) -}}
{{- macros.section('resolvers', haproxy_resolvers) -}}
{{- macros.section('mailers', haproxy_mailers) -}}
{{- macros.section('userlist', haproxy_userlists) -}}
{{- macros.section('frontend', haproxy_frontends) -}}
{{- macros.section('backend', haproxy_backends) -}}
{{- macros.section('listen', haproxy_listens) -}}
115 changes: 0 additions & 115 deletions templates/listen.cfg

This file was deleted.

18 changes: 0 additions & 18 deletions templates/resolvers.cfg

This file was deleted.

14 changes: 0 additions & 14 deletions templates/userlist.cfg

This file was deleted.

4 changes: 4 additions & 0 deletions test/filter_plugins/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
154 changes: 154 additions & 0 deletions test/filter_plugins/fixtures/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
haproxy_defaults:
log: global
mode: http
option:
- httplog
- dontlognull
timeout:
connect: 5000
client: 50000
server: 50000
errorfile:
400: /etc/haproxy/errors/400.http
403: /etc/haproxy/errors/403.http
408: /etc/haproxy/errors/408.http
500: /etc/haproxy/errors/500.http
502: /etc/haproxy/errors/502.http
503: /etc/haproxy/errors/503.http
504: /etc/haproxy/errors/504.http


haproxy_global:
log:
/dev/log:
- local0
- local1 notice
chroot: /var/lib/haproxy
stats:
socket: /run/haproxy/admin.sock mode 660 level admin
timeout: 30s
user: haproxy
group: haproxy
daemon: true
ca-base: /etc/ssl/certs
crt-base: /etc/ssl/private
ssl-default-bind-ciphers: ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
ssl-default-bind-options: no-sslv3


haproxy_mailers:
mailers1:
mailer:
smtp1: 192.0.2.1:587
smtp2: 192.0.2.2:587
mailers2:
mailer:
smtp1: 192.51.100.1:587
smtp2: 192.51.100.2:587


haproxy_peers:
peers1:
peer:
haproxy1: 192.0.2.1:1024
haproxy2: 192.0.2.2:1024
haproxy3: 192.0.2.3:1024
peers2:
peer:
haproxy1: 192.51.100.1:1024
haproxy2: 192.51.100.2:1024
haproxy3: 192.51.100.3:1024


haproxy_resolvers:
resolvers1:
nameserver:
dns1: 192.0.2.1:53
dns2: 192.0.2.2:53
resolve_retries: 3
timeout:
retry: 1s
hold:
valid: 10s
resolvers2:
nameserver:
dns1: 192.51.100.1:53
dns2: 192.51.100.2:53
resolve_retries: 3
timeout:
retry: 1s
hold:
valid: 10s


haproxy_userlists:
userlist1:
group:
- G1 users tiger,scott
- G2 users xdb,scott
user:
- tiger password $6$k6y3o.eP$JlKBx9za9667qe4(...)xHSwRv6J.C0/D7cV91
- scott insecure-password elgato
- xdb insecure-password hello
userlist2:
group:
- G1
- G2
user:
- tiger password $6$k6y3o.eP$JlKBx(...)xHSwRv6J.C0/D7cV91 groups G1
- scott insecure-password elgato groups G1,G2
- xdb insecure-password hello groups G2


haproxy_listens:
listen1:
mode: http
bind:
- :80
- :443 ssl crt /etc/haproxy/site.pem
acl:
- invalid_src src 0.0.0.0/7 224.0.0.0/3
- invalid_src src_port 0:1023
- local_dst hdr(host) -i localhost
- es req.fhdr(accept-language),language(es;fr;en) -m str es
- fr req.fhdr(accept-language),language(es;fr;en) -m str fr
- en req.fhdr(accept-language),language(es;fr;en) -m str en
tcp-request:
connection:
- accept if { src -f /etc/haproxy/whitelist.lst }
- reject if { src_conn_rate gt 10 }
- track-sc0 src
block:
- if invalid_src || local_dst
http-request:
deny:
- if invalid_src || local_dst
reqdel:
- ^X-Forwarded-For:.*
- ^Cookie:.*SERVER=
reqpass:
- ^Host:\ www.private\.local
reqadd:
- X-Proto:\ SSL if is-ssl
redirect:
- prefix https://mysite.com set-cookie SEEN=1 if !cookie_set
- prefix https://mysite.com if login_page !secure
- prefix http://mysite.com drop-query if login_page !uid_given
- location http://mysite.com/ if !login_page secure
- location / clear-cookie USERID= if logout
use_backend:
- backend1 if is-app1
- backend2 if is-app2
use-server:
- www if { req_ssl_sni -i www.example.com }
- mail if { req_ssl_sni -i mail.example.com }
- imap if { req_ssl_sni -i imap.example.com }
server:
www: 192.0.2.1:443 weight 0
mail: 192.0.2.1:587 weight 0
imap: 192.0.2.1:993 weight 0
default: 192.0.2.2:443 check
listen2:
no log: true
disabled: false
125 changes: 125 additions & 0 deletions test/filter_plugins/fixtures/expected.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
haproxy_defaults:
- errorfile 400 /etc/haproxy/errors/400.http
- errorfile 403 /etc/haproxy/errors/403.http
- errorfile 408 /etc/haproxy/errors/408.http
- errorfile 500 /etc/haproxy/errors/500.http
- errorfile 502 /etc/haproxy/errors/502.http
- errorfile 503 /etc/haproxy/errors/503.http
- errorfile 504 /etc/haproxy/errors/504.http
- log global
- mode http
# Preserve order within YAML sequences.
- option httplog
- option dontlognull
- timeout client 50000
- timeout connect 5000
- timeout server 50000

haproxy_global:
- ca-base /etc/ssl/certs
- chroot /var/lib/haproxy
- crt-base /etc/ssl/private
- daemon
- group haproxy
- log /dev/log local0
- log /dev/log local1 notice
- ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
- ssl-default-bind-options no-sslv3
- stats socket /run/haproxy/admin.sock mode 660 level admin
- stats timeout 30s
- user haproxy

haproxy_mailers:
mailers1:
- mailer smtp1 192.0.2.1:587
- mailer smtp2 192.0.2.2:587
mailers2:
- mailer smtp1 192.51.100.1:587
- mailer smtp2 192.51.100.2:587

haproxy_peers:
peers1:
- peer haproxy1 192.0.2.1:1024
- peer haproxy2 192.0.2.2:1024
- peer haproxy3 192.0.2.3:1024
peers2:
- peer haproxy1 192.51.100.1:1024
- peer haproxy2 192.51.100.2:1024
- peer haproxy3 192.51.100.3:1024

haproxy_resolvers:
resolvers1:
- hold valid 10s
- nameserver dns1 192.0.2.1:53
- nameserver dns2 192.0.2.2:53
- resolve_retries 3
- timeout retry 1s
resolvers2:
- hold valid 10s
- nameserver dns1 192.51.100.1:53
- nameserver dns2 192.51.100.2:53
- resolve_retries 3
- timeout retry 1s

haproxy_userlists:
userlist1:
- group G1 users tiger,scott
- group G2 users xdb,scott
- user tiger password $6$k6y3o.eP$JlKBx9za9667qe4(...)xHSwRv6J.C0/D7cV91
- user scott insecure-password elgato
- user xdb insecure-password hello
userlist2:
- group G1
- group G2
- user tiger password $6$k6y3o.eP$JlKBx(...)xHSwRv6J.C0/D7cV91 groups G1
- user scott insecure-password elgato groups G1,G2
- user xdb insecure-password hello groups G2

haproxy_listens:
listen1:
# rank -1
- acl invalid_src src 0.0.0.0/7 224.0.0.0/3
- acl invalid_src src_port 0:1023
- acl local_dst hdr(host) -i localhost
- acl es req.fhdr(accept-language),language(es;fr;en) -m str es
- acl fr req.fhdr(accept-language),language(es;fr;en) -m str fr
- acl en req.fhdr(accept-language),language(es;fr;en) -m str en
# rank 0, in alphabetical order
- bind :80
- bind :443 ssl crt /etc/haproxy/site.pem
- mode http
# rank 2
- tcp-request connection accept if { src -f /etc/haproxy/whitelist.lst }
- tcp-request connection reject if { src_conn_rate gt 10 }
- tcp-request connection track-sc0 src
# rank 3
- block if invalid_src || local_dst
# rank 4
- http-request deny if invalid_src || local_dst
# rank 5, in order
- reqdel ^X-Forwarded-For:.*
- reqdel ^Cookie:.*SERVER=
- reqpass ^Host:\ www.private\.local
# rank 6
- reqadd X-Proto:\ SSL if is-ssl
# rank 7
- redirect prefix https://mysite.com set-cookie SEEN=1 if !cookie_set
- redirect prefix https://mysite.com if login_page !secure
- redirect prefix http://mysite.com drop-query if login_page !uid_given
- redirect location http://mysite.com/ if !login_page secure
- redirect location / clear-cookie USERID= if logout
# rank 8
- use_backend backend1 if is-app1
- use_backend backend2 if is-app2
# rank 9
- use-server www if { req_ssl_sni -i www.example.com }
- use-server mail if { req_ssl_sni -i mail.example.com }
- use-server imap if { req_ssl_sni -i imap.example.com }
# rank 100
- server default 192.0.2.2:443 check
- server imap 192.0.2.1:993 weight 0
- server mail 192.0.2.1:587 weight 0
- server www 192.0.2.1:443 weight 0
listen2:
- no log
44 changes: 44 additions & 0 deletions test/filter_plugins/haproxy_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import os

import pytest
import yaml

from filter_plugins import haproxy


@pytest.fixture
def config():
config_file = os.path.join(os.path.dirname(__file__), 'fixtures', 'config.yml')
return yaml.load(open(config_file))


@pytest.fixture
def expected():
results_file = os.path.join(os.path.dirname(__file__), 'fixtures', 'expected.yml')
return yaml.load(open(results_file))


@pytest.mark.parametrize(
('section',),
[
('haproxy_defaults',),
('haproxy_global',),
]
)
def test_haproxy_single_sections(config, expected, section):
assert list(haproxy.to_haproxy(config[section])) == expected[section]


@pytest.mark.parametrize(
('section',),
[
('haproxy_mailers',),
('haproxy_peers',),
('haproxy_resolvers',),
('haproxy_userlists',),
('haproxy_listens',),
]
)
def test_haproxy_multiple_sections(config, expected, section):
for group, options in config[section].items():
assert list(haproxy.to_haproxy(options)) == expected[section][group]
35 changes: 12 additions & 23 deletions test/integration/default/default.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
---
- hosts: localhost

roles:
- { role: ansible-haproxy,
haproxy_frontends: [
{
name: 'fe-mysupersite',
ip: '*',
port: '80',
maxconn: '1000',
default_backend: 'be-mysupersite',
}
],
haproxy_backends: [
{
name: 'be-mysupersite',
description: 'mysupersite is really cool',
servers: [
{
name: 'be-mysupersite-01',
ip: '192.168.1.100'
}
]
}
] }
- role: ansible-haproxy
haproxy_frontends:
fe-mysupersite:
bind: '*:80'
maxconn: 1000
default_backend: be-mysupersite

haproxy_backends:
be-mysupersite:
description: mysupersite is really cool
server:
be-mysupersite-01: 192.168.1.100
10 changes: 10 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[tox]
envlist = py27,py35,py36

[testenv]
deps =
PyYAML
pytest
pytest-cov
commands =
pytest -vv --cov=filter_plugins
233 changes: 0 additions & 233 deletions vars/main.yml
Original file line number Diff line number Diff line change
@@ -1,235 +1,2 @@
---

empty: true
#haproxy_global:
# chroot:
# pidfile:
# maxconn:
# user:
# uid:
# group:
# gid:
# daemon:
# nbproc:
# spread_checks:
# stats:
# socket:
# timeout:
# log:
# - address:
# facility:
# level:
# minlevel:
# format:
# ssl_default_bind_options:
# ssl_default_bind_ciphers:
# tune:
# chksize: 32768
# ssl:
# default-dh-param: 2048
# zlib:
# memlevel: 9
#
#haproxy_defaults:
# mode:
# log:
# - address:
# facility:
# level:
# minlevel:
# format:
# options:
# - <option>
# retries:
# timeout:
# - param:
# value:
# balance:
# cookie:
# maxconn:
# stats:
# enabled:
# hide_version:
# uri:
# realm:
# auth:
# refresh:
# compression:
# algo:
# type:
# offload:
# http_check:
# disable_on_404:
# expect:
# send_state:
#
#haproxy_resolvers:
# - name:
# nameservers:
# - name:
# ip:
# port:
# hold:
# - status:
# period:
# resolve_retries:
# timeout_retry:
#
#haproxy_frontends:
# - name:
# ip:
# bind:
# - 192.168.1.1:80
# - 192.168.1.2:81
# ssl:
# cert: /etc/ssl/private/cert.pem
# ciphers: 'RC4-SHA:AES128-SHA:AES:!ADH:!aNULL:!DH:!EDH:!eNULL'
# maxconn:
# monitor:
# uri:
# fail:
# - <condition>
# condition:
# -
# acl:
# - name:
# condition:
# http_request:
# - action:
# param:
# condition:
# http_response:
# - action:
# param:
# condition:
# rate_limit_sessions:
# block:
# -
# options:
# - forwardfor
# default_backend:
# use_backend:
# - name:
# condition:
# timeout:
# - param:
# value:
# reqadd:
# - "X-RequestHeader1:\\ some-value"
# - "X-RequestHeader2:\\ some-value"
# rspadd:
# - "X-ResponseHeader1:\\ some-value"
# - "X-ResponseHeader2:\\ some-value"
# reqrep:
# - "^Host:\ www.(.*)$ Host:\ \1 if host_www"
# reqirep:
# - "^Host:\ www.(.*)$ Host:\ \1 if host_www"
# rsprep:
# - "^Location:\ 127.0.0.1:8080 Location:\ www.mydomain.com"
# rspirep:
# - "^Location:\ 127.0.0.1:8080 Location:\ www.mydomain.com"
# force_persist:
#
#haproxy_backends:
# - name:
# disabled:
# description:
# balance:
# cookie:
# log:
# retries:
# contimeout:
# NOTE: contimeout is deprecated
# http://cbonte.github.io/haproxy-dconv/configuration-1.5.html#4-contimeout
# http_send_name_header:
# http_check_expect:
# - condition
# acl:
# - name:
# condition:
# servers:
# - name:
# ip:
# port:
# maxconn:
# params:
# - param1
# options:
# - forwardfor
# timeout:
# - param:
# value:
# appsession: 'JSESSIONID len 52 timeout 3h'
# errorfile:
# - code:
# file:
# reqadd:
# - "X-RequestHeader1:\\ some-value"
# - "X-RequestHeader2:\\ some-value"
# rspadd:
# - "X-ResponseHeader1:\\ some-value"
# - "X-ResponseHeader2:\\ some-value"
# reqrep:
# - "^Host:\ www.(.*)$ Host:\ \1 if host_www"
# reqirep:
# - "^Host:\ www.(.*)$ Host:\ \1 if host_www"
# rsprep:
# - "^Location:\ 127.0.0.1:8080 Location:\ www.mydomain.com"
# rspirep:
# - "^Location:\ 127.0.0.1:8080 Location:\ www.mydomain.com"
# force_persist:
#
#haproxy_listen:
# - name:
# bind:
# ssl:
# cert: /etc/ssl/private/cert.pem
# ciphers: 'RC4-SHA:AES128-SHA:AES:!ADH:!aNULL:!DH:!EDH:!eNULL'
# disabled:
# description:
# maxconn:
# balance:
# cookie:
# log:
# retries:
# http_send_name_header:
# http_check_expect:
# - condition
# acl:
# - name:
# condition:
# servers:
# - name:
# ip:
# port:
# maxconn:
# params:
# - param1
# options:
# - forwardfor
# timeout:
# - param:
# value:
# reqadd:
# - "X-RequestHeader1:\\ some-value"
# - "X-RequestHeader2:\\ some-value"
# rspadd:
# - "X-ResponseHeader1:\\ some-value"
# - "X-ResponseHeader2:\\ some-value"
# reqrep:
# - "^Host:\ www.(.*)$ Host:\ \1 if host_www"
# reqirep:
# - "^Host:\ www.(.*)$ Host:\ \1 if host_www"
# rsprep:
# - "^Location:\ 127.0.0.1:8080 Location:\ www.mydomain.com"
# rspirep:
# - "^Location:\ 127.0.0.1:8080 Location:\ www.mydomain.com"
# appsession: 'JSESSIONID len 52 timeout 3h'
# stats:
# enabled:
# hide_version:
# uri:
# realm:
# auth:
# refresh:
# force_persist: