Implement rate-limiting to the format `listen`

Moving rate_limit logic to the haproxy/files/_rate_limit.cfg

Change-Id: Iefc4860793a47934703eae8efdcc2f41761f5ae5
Related-Prod: PROD-24396
diff --git a/.kitchen.yml b/.kitchen.yml
index e82b842..8ee5116 100644
--- a/.kitchen.yml
+++ b/.kitchen.yml
@@ -60,4 +60,9 @@
     provisioner:
       pillars-from-files:
         haproxy.sls: tests/pillar/stats.sls
+
+  - name: single_rate_limiting
+    provisioner:
+      pillars-from-files:
+        haproxy.sls: tests/pillar/single_rate_limiting.sls
 # vim: ft=yaml sw=2 ts=2 sts=2 tw=125
diff --git a/.travis.yml b/.travis.yml
index d5b4c64..cd01c16 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,16 +19,19 @@
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-2016.3/salt:2018_11_19 SUITE=single-contrail
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-2016.3/salt:2018_11_19 SUITE=single-general-service
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-2016.3/salt:2018_11_19 SUITE=single-openstack-service
+    - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-2016.3/salt:2018_11_19 SUITE=single-rate-limiting
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-2016.3/salt:2018_11_19 SUITE=stats
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-2017.7/salt:2018_11_19 SUITE=admin
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-2017.7/salt:2018_11_19 SUITE=single-contrail
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-2017.7/salt:2018_11_19 SUITE=single-general-service
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-2017.7/salt:2018_11_19 SUITE=single-openstack-service
+    - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-2017.7/salt:2018_11_19 SUITE=single-rate-limiting
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-2017.7/salt:2018_11_19 SUITE=stats
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-stable/salt:2018_11_19 SUITE=admin
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-stable/salt:2018_11_19 SUITE=single-contrail
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-stable/salt:2018_11_19 SUITE=single-general-service
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-stable/salt:2018_11_19 SUITE=single-openstack-service
+    - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-stable/salt:2018_11_19 SUITE=single-rate-limiting
     - PLATFORM=docker-dev-local.docker.mirantis.net/epcim/salt/saltstack-ubuntu-xenial-salt-stable/salt:2018_11_19 SUITE=stats
 
 before_script:
diff --git a/README.rst b/README.rst
index 9710e5f..61a508e 100644
--- a/README.rst
+++ b/README.rst
@@ -622,6 +622,26 @@
           - auth admin1:AdMiN123
           rate_limit_sessions: 1000
 
+Implement rate limiting, to prevent excessive requests
+using 'format: listen'
+
+.. code-block:: yaml
+
+  haproxy:
+    proxy:
+      ...
+      listen:
+        nova_metadata_api:
+          ...
+          rate_limit:
+            duration: 3s
+            enabled: true
+            requests: 60
+            track: connection
+          servers:
+            ...
+
+
 Read more
 =========
 
diff --git a/haproxy/files/_rate_limit.cfg b/haproxy/files/_rate_limit.cfg
new file mode 100644
index 0000000..499c6e4
--- /dev/null
+++ b/haproxy/files/_rate_limit.cfg
@@ -0,0 +1,17 @@
+
+  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
+  {%- set stick_table_found = { 'val': false } %}
+  {%- for item in listen.get('sticks', []) if item.startswith('stick-table ') %}
+  {%- do stick_table_found.update({'val': true}) %}
+  {%- endfor %}
+  {%- if not stick_table_found.val %}
+  stick-table type string size {{ listen.rate_limit.get('size', '100k') }} store gpc0_rate({{ listen.rate_limit.get('duration', '60s') }})
+  {%- endif %}
+  {%- 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
diff --git a/haproxy/files/haproxy.cfg b/haproxy/files/haproxy.cfg
index 3927794..08bb0bc 100644
--- a/haproxy/files/haproxy.cfg
+++ b/haproxy/files/haproxy.cfg
@@ -250,6 +250,9 @@
   rate-limit sessions {{ listen.rate_limit_sessions }}
   {%- endif %}
   {%- endif %}
+  {%- if listen.rate_limit is defined and listen.rate_limit.get('enabled', False) %}
+  {%- include "haproxy/files/_rate_limit.cfg" %}
+  {%- endif %}
   {%- for server in listen.get('servers', []) %}
     {%- set port_range_length=server.get('port_range_length', 1) %}
     {%- set port_range_start_offset=server.get('port_range_start_offset', 0) %}
@@ -257,6 +260,12 @@
   server {{ server.name }}{% if worker_port > 0 %}p{{ worker_port }}{% endif %} {{ server.host }}:{{ server.port + worker_port }} {{ server.get('params', '') }}
     {%- endfor %}
   {%- endfor %}
+  {%- 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 %}
 {%- endif %}
 {%- endfor %}
@@ -297,16 +306,7 @@
   {%- 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 {{ listen.rate_limit.get('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
+  {%- include "haproxy/files/_rate_limit.cfg" %}
   {%- endif %}
 
   default_backend {{ listen_name }}-backend
diff --git a/tests/pillar/single_rate_limiting.sls b/tests/pillar/single_rate_limiting.sls
index fe13de9..921bc0d 100644
--- a/tests/pillar/single_rate_limiting.sls
+++ b/tests/pillar/single_rate_limiting.sls
@@ -33,7 +33,34 @@
           params: check inter 10s fastinter 2s downinter 3s rise 3 fall 3
           port: 8775
         type: http
-
+      nova_novnc:
+        binds:
+        - address: 127.0.0.1
+          port: 8776
+        format: listen
+        options:
+        - httpchk
+        - httpclose
+        - httplog
+        rate_limit:
+          duration: 5s
+          enabled: true
+          requests: 60
+          track: connection
+        servers:
+        - host: 127.0.0.1
+          name: ctl01
+          params: check inter 10s fastinter 2s downinter 3s rise 3 fall 3
+          port: 8776
+        - host: 127.0.0.1
+          name: ctl02
+          params: check inter 10s fastinter 2s downinter 3s rise 3 fall 3
+          port: 8776
+        - host: 127.0.0.1
+          name: ctl03
+          params: check inter 10s fastinter 2s downinter 3s rise 3 fall 3
+          port: 8776
+        type: http
 # For haproxy/meta/sensu.yml
 linux:
   network: