Implement rate limiting (#43)

* Implement ratelimiting
- by sending back '429 Too Many Requests' error message
- Error message is a HTTP/1.1 response, so contrail link local proxy works with it too.
- HAProxy sends back HTTP/1.0 responses, so using a template is required.

Only possible when using front-/backend configuration layout.

* Add options in frontend and backend.

Options are filtered by a blacklist of invalid options, as per haproxy manual.

* Add sls test for rate limiting configuration

* Update readme with rate limiting example
diff --git a/README.rst b/README.rst
index 06fd886..09bcdd7 100644
--- a/README.rst
+++ b/README.rst
@@ -427,6 +427,30 @@
             port: 8443
             params: 'maxconn 256'
 
+Implement rate limiting, to prevent excessive requests
+This feature only works if using 'format: end'
+
+.. code-block:: yaml
+  haproxy:
+    proxy:
+      ...
+      listen:
+        nova_metadata_api:
+          ...
+          format: end
+          options:
+          - httpchk
+          - httpclose
+          - httplog
+          rate_limit:
+            duration: 900s
+            enabled: true
+            requests: 125
+            track: content
+          servers:
+            ...
+          type: http
+
 Read more
 =========
 
diff --git a/haproxy/files/errors/429.http11 b/haproxy/files/errors/429.http11
new file mode 100644
index 0000000..6ba5890
--- /dev/null
+++ b/haproxy/files/errors/429.http11
@@ -0,0 +1,9 @@
+HTTP/1.1 429 Too Many Requests
+Cache-Control: no-cache
+Connection: close
+Content-Type: text/html
+Content-Length: 118
+
+<html><body><h1>429 Too Many Requests</h1>
+You have sent too many requests in a given amount of time.
+</body></html>
diff --git a/haproxy/files/haproxy.cfg b/haproxy/files/haproxy.cfg
index f86f379..4f9bf0d 100644
--- a/haproxy/files/haproxy.cfg
+++ b/haproxy/files/haproxy.cfg
@@ -1,4 +1,4 @@
-{%- from "haproxy/map.jinja" import proxy with context -%}
+{%- from "haproxy/map.jinja" import proxy, invalid_section_options with context -%}
 
 global
 {%- if proxy.nbproc is defined %}
@@ -232,25 +232,53 @@
   redirect {% if redirect.code is defined %} code {{ redirect.code }} {% endif %} location {{ redirect.location }} if { {{ condition.type }} {{ condition.condition }} }
   {%- endfor %}
   {%- endfor %}
+
+  {#- Add options in the frontend section that make sense. #}
+  {%- for option in listen.get('options', []) %}
+  {%- if option not in invalid_section_options.frontend %}
+  option {{ option }}
+  {%- endif %}
+  {%- endfor %}
+
   {%- for acl in listen.get('acls', []) %}
   {%- for condition in acl.get('conditions', []) %}
   acl {{ acl.name }} {{ condition.type }} {{ condition.condition }}
   {%- endfor %}
+
   {%- if listen_name == 'service_proxy' %}
   use_backend {{ acl.backend|default(acl.name, true) }} if {{ acl.name }}
   {% else %}
   use_backend {{ acl.name }}-backend if {{ acl.name }}
   {% endif %}
   {%- endfor %}
+
+  {%- if listen.rate_limit is defined and listen.rate_limit.get('enabled', False) %}
+  tcp-request inspect-delay 5s
+  acl too_many_requests sc0_gpc0_rate() gt {{ listen.rate_limit.get('requests', 100) }}
+  acl mark_seen sc0_inc_gpc0 gt 0
+  stick-table type string size 100k store gpc0_rate({{ listen.rate_limit.get('duration', '60s') }})
+  {%- if listen.rate_limit.get('track', 'content') == 'content' %}
+  tcp-request content track-sc0 {{ listen.rate_limit.get('header', 'hdr(X-Forwarded-For)') }} if ! too_many_requests
+  {%- else %}
+  tcp-request connection track-sc0 {{ listen.rate_limit.get('tracking_key', 'src') }} if ! too_many_requests
+  {%- endif %}
+  use_backend {{ listen_name }}-rate_limit if mark_seen too_many_requests
+  {%- endif %}
+
   default_backend {{ listen_name }}-backend
 
 backend {{ listen_name }}-backend
   {%- if listen.get('type', None) == 'http' %}
   balance {{ listen.get('balance', 'roundrobin') }}
   {%- endif %}
+
+  {#- Add options in the backend section that make sense. #}
   {%- for option in listen.get('options', []) %}
+  {%- if option not in invalid_section_options.backend %}
   option {{ option }}
+  {%- endif %}
   {%- endfor %}
+
   {%- for server in listen.get('servers', []) %}
   server {{ server.get('name', server.host) }} {{ server.host }}:{{ server.port }} {{ server.get('params', '') }}
   {%- endfor %}
@@ -267,5 +295,13 @@
   {%- endfor %}
 {%- endfor %}
 {%- endif %}
+
+{%- if listen.rate_limit is defined and listen.rate_limit.get('enabled', False) %}
+
+backend {{ listen_name }}-rate_limit
+  timeout tarpit {{ listen.rate_limit.get('tarpit_timeout', '2s') }}
+  errorfile 500 /etc/haproxy/errors/429.http11
+  http-request tarpit
+{%- endif %}
 {%- endif %}
 {%- endfor %}
diff --git a/haproxy/map.jinja b/haproxy/map.jinja
index 2fdb2b3..966f342 100644
--- a/haproxy/map.jinja
+++ b/haproxy/map.jinja
@@ -10,3 +10,45 @@
         'stats_socket': '/var/lib/haproxy/stats',
     },
 }, merge=salt['pillar.get']('haproxy:proxy')) %}
+{% set invalid_section_options = {
+    'frontend': [
+        'abortonclose',
+        'accept-invalid-http-response',
+        'allbackups',
+        'checkcache',
+        'external-check',
+        'httpchk',
+        'ldap-check',
+        'log-health-checks',
+        'mysql-check',
+        'persist',
+        'pgsql-check',
+        'prefer-last-server',
+        'redis-check',
+        'redispatch',
+        'smtpchk',
+        'srvtcpka',
+        'ssl-hello-chk',
+        'tcp-check',
+        'tcp-smart-connect',
+        'tcpka',
+        'tcplog',
+        'transparent',
+    ],
+    'backend': [
+        'accept-invalid-http-request',
+        'clitcpka',
+        'contstats',
+        'dontlog-normal',
+        'dontlognull',
+        'http-ignore-probes',
+        'http-use-proxy-header',
+        'log-separate-errors',
+        'logasap',
+        'socket-stats',
+        'tcp-smart-accept',
+        'tcpka',
+        'tcplog',
+    ],
+}
+%}
diff --git a/haproxy/proxy.sls b/haproxy/proxy.sls
index 55792c6..07be51a 100644
--- a/haproxy/proxy.sls
+++ b/haproxy/proxy.sls
@@ -27,6 +27,17 @@
   - require:
     - pkg: haproxy_packages
 
+rate_limit_error_file:
+  file.managed:
+  - name: /etc/haproxy/errors/429.http11
+  - user: root
+  - group: root
+  - mode: 644
+  - source: salt://haproxy/files/errors/429.http11
+  - template: jinja
+  - require:
+    - pkg: haproxy_packages
+
 haproxy_status_packages:
   pkg.installed:
   - pkgs:
diff --git a/tests/pillar/single_rate_limiting.sls b/tests/pillar/single_rate_limiting.sls
new file mode 100644
index 0000000..fe13de9
--- /dev/null
+++ b/tests/pillar/single_rate_limiting.sls
@@ -0,0 +1,40 @@
+haproxy:
+  proxy:
+    enabled: true
+    mode: tcp
+    logging: syslog
+    max_connections: 1024
+    listen:
+      nova_metadata_api:
+        binds:
+        - address: 127.0.0.1
+          port: 8775
+        format: end
+        options:
+        - httpchk
+        - httpclose
+        - httplog
+        rate_limit:
+          duration: 900s
+          enabled: true
+          requests: 125
+          track: content
+        servers:
+        - host: 127.0.0.1
+          name: ctl01
+          params: check inter 10s fastinter 2s downinter 3s rise 3 fall 3
+          port: 8775
+        - host: 127.0.0.1
+          name: ctl02
+          params: check inter 10s fastinter 2s downinter 3s rise 3 fall 3
+          port: 8775
+        - host: 127.0.0.1
+          name: ctl03
+          params: check inter 10s fastinter 2s downinter 3s rise 3 fall 3
+          port: 8775
+        type: http
+
+# For haproxy/meta/sensu.yml
+linux:
+  network:
+    fqdn: linux.ci.local