Repository: ansible/ansible

Issue:
Add support for CONNECTION LIMIT to postgresql_db
##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
postgresql_db

##### ANSIBLE VERSION
```
2.5.2
```

##### SUMMARY
Just like with https://github.com/ansible/ansible-modules-core/issues/1570 or https://github.com/ansible/ansible/commit/8a2f9b7e28106037a0bfe6d142b61a2e2ee9e984 Ansible received support to configure the connection_limit for postgresql_user it's also possible to limit the number of connection per database. 

See: 
 * https://www.postgresql.org/docs/9.6/static/sql-createdatabase.html
 * https://www.postgresql.org/docs/9.6/static/sql-alterdatabase.html


Currently Ansible does not allow setting this option for postgresql_db.


Discussion:
Files identified in the description:
* [lib/ansible/modules/database/postgresql/postgresql_db.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/database/postgresql/postgresql_db.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @matburt
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@frittentheke Thanks for requesting this feature. @maveric-tellrv Would you like to raise a PR for this ?

Resolving patch:
diff --git a/lib/ansible/modules/database/postgresql/postgresql_db.py b/lib/ansible/modules/database/postgresql/postgresql_db.py
--- a/lib/ansible/modules/database/postgresql/postgresql_db.py
+++ b/lib/ansible/modules/database/postgresql/postgresql_db.py
@@ -83,6 +83,11 @@
     type: str
     default: postgres
     version_added: "2.5"
+  conn_limit:
+    description:
+      - Specifies the database connection limit.
+    type: str
+    version_added: '2.8'
 notes:
 - State C(dump) and C(restore) don't require I(psycopg2) since version 2.8.
 author: "Ansible Core Team"
@@ -104,6 +109,12 @@
     lc_ctype: de_DE.UTF-8
     template: template0
 
+# Note: Default limit for the number of concurrent connections to a specific database is "-1", which means "unlimited"
+- name: Create a new database with name "acme" which has a limit of 100 concurrent connections
+  postgresql_db:
+    name: acme
+    conn_limit: "100"
+
 - name: Dump an existing database to a file
   postgresql_db:
     name: acme
@@ -162,6 +173,14 @@ def set_owner(cursor, db, owner):
     return True
 
 
+def set_conn_limit(cursor, db, conn_limit):
+    query = "ALTER DATABASE %s CONNECTION LIMIT %s" % (
+            pg_quote_identifier(db, 'database'),
+            conn_limit)
+    cursor.execute(query)
+    return True
+
+
 def get_encoding_id(cursor, encoding):
     query = "SELECT pg_char_to_encoding(%(encoding)s) AS encoding_id;"
     cursor.execute(query, {'encoding': encoding})
@@ -172,7 +191,7 @@ def get_db_info(cursor, db):
     query = """
     SELECT rolname AS owner,
     pg_encoding_to_char(encoding) AS encoding, encoding AS encoding_id,
-    datcollate AS lc_collate, datctype AS lc_ctype
+    datcollate AS lc_collate, datctype AS lc_ctype, pg_database.datconnlimit AS conn_limit
     FROM pg_database JOIN pg_roles ON pg_roles.oid = pg_database.datdba
     WHERE datname = %(db)s
     """
@@ -195,8 +214,8 @@ def db_delete(cursor, db):
         return False
 
 
-def db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype):
-    params = dict(enc=encoding, collate=lc_collate, ctype=lc_ctype)
+def db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit):
+    params = dict(enc=encoding, collate=lc_collate, ctype=lc_ctype, conn_limit=conn_limit)
     if not db_exists(cursor, db):
         query_fragments = ['CREATE DATABASE %s' % pg_quote_identifier(db, 'database')]
         if owner:
@@ -209,6 +228,8 @@ def db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype):
             query_fragments.append('LC_COLLATE %(collate)s')
         if lc_ctype:
             query_fragments.append('LC_CTYPE %(ctype)s')
+        if conn_limit:
+            query_fragments.append("CONNECTION LIMIT %(conn_limit)s" % {"conn_limit": conn_limit})
         query = ' '.join(query_fragments)
         cursor.execute(query, params)
         return True
@@ -230,13 +251,19 @@ def db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype):
                 'Changing LC_CTYPE is not supported.'
                 'Current LC_CTYPE: %s' % db_info['lc_ctype']
             )
-        elif owner and owner != db_info['owner']:
-            return set_owner(cursor, db, owner)
         else:
-            return False
+            changed = False
 
+            if owner and owner != db_info['owner']:
+                changed = set_owner(cursor, db, owner)
 
-def db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype):
+            if conn_limit and conn_limit != str(db_info['conn_limit']):
+                changed = set_conn_limit(cursor, db, conn_limit)
+
+            return changed
+
+
+def db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit):
     if not db_exists(cursor, db):
         return False
     else:
@@ -250,6 +277,8 @@ def db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype):
             return False
         elif owner and owner != db_info['owner']:
             return False
+        elif conn_limit and conn_limit != str(db_info['conn_limit']):
+            return False
         else:
             return True
 
@@ -397,6 +426,7 @@ def main():
         target_opts=dict(type='str', default=''),
         maintenance_db=dict(type='str', default="postgres"),
         session_role=dict(type='str'),
+        conn_limit=dict(type='str', default='')
     )
 
     module = AnsibleModule(
@@ -416,6 +446,7 @@ def main():
     changed = False
     maintenance_db = module.params['maintenance_db']
     session_role = module.params["session_role"]
+    conn_limit = module.params['conn_limit']
 
     raw_connection = state in ("dump", "restore")
 
@@ -481,7 +512,7 @@ def main():
             if state == "absent":
                 changed = db_exists(cursor, db)
             elif state == "present":
-                changed = not db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype)
+                changed = not db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit)
             module.exit_json(changed=changed, db=db)
 
         if state == "absent":
@@ -492,7 +523,7 @@ def main():
 
         elif state == "present":
             try:
-                changed = db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype)
+                changed = db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit)
             except SQLParseError as e:
                 module.fail_json(msg=to_native(e), exception=traceback.format_exc())
 


Test patch:
diff --git a/test/integration/targets/postgresql/tasks/main.yml b/test/integration/targets/postgresql/tasks/main.yml
--- a/test/integration/targets/postgresql/tasks/main.yml
+++ b/test/integration/targets/postgresql/tasks/main.yml
@@ -101,14 +101,15 @@
 #        that: "result.stdout_lines[-1] == '(0 rows)'"
 
 #
-# Test encoding, collate, ctype, template options
+# Test conn_limit, encoding, collate, ctype, template options
 #
-- name: Create a DB with encoding, collate, ctype, and template options
+- name: Create a DB with conn_limit, encoding, collate, ctype, and template options
   become_user: "{{ pg_user }}"
   become: True
   postgresql_db:
     name: '{{ db_name }}'
     state: 'present'
+    conn_limit: '100'
     encoding: 'LATIN1'
     lc_collate: 'pt_BR{{ locale_latin_suffix }}'
     lc_ctype: 'es_ES{{ locale_latin_suffix }}'
@@ -118,7 +119,7 @@
 - name: Check that the DB has all of our options
   become_user: "{{ pg_user }}"
   become: True
-  shell: echo "select datname, pg_encoding_to_char(encoding), datcollate, datctype from pg_database where datname = '{{ db_name }}';" | psql -d postgres
+  shell: echo "select datname, datconnlimit, pg_encoding_to_char(encoding), datcollate, datctype from pg_database where datname = '{{ db_name }}';" | psql -d postgres
   register: result
 
 - assert:
@@ -129,6 +130,7 @@
       - "'es_ES' in result.stdout_lines[-2]"
       - "'UTF8' not in result.stdout_lines[-2]"
       - "'en_US' not in result.stdout_lines[-2]"
+      - "'100' in result.stdout_lines[-2]"
 
 - name: Check that running db cration with options a second time does nothing
   become_user: "{{ pg_user }}"
@@ -136,6 +138,7 @@
   postgresql_db:
     name: '{{ db_name }}'
     state: 'present'
+    conn_limit: '100'
     encoding: 'LATIN1'
     lc_collate: 'pt_BR{{ locale_latin_suffix }}'
     lc_ctype: 'es_ES{{ locale_latin_suffix }}'
@@ -166,6 +169,35 @@
     that:
       - 'result.failed == True'
 
+- name: Check that changing the conn_limit actually works
+  become_user: "{{ pg_user }}"
+  become: True
+  postgresql_db:
+    name: '{{ db_name }}'
+    state: 'present'
+    conn_limit: '200'
+    encoding: 'LATIN1'
+    lc_collate: 'pt_BR{{ locale_latin_suffix }}'
+    lc_ctype: 'es_ES{{ locale_latin_suffix }}'
+    template: 'template0'
+    login_user: "{{ pg_user }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == True'
+
+- name: Check that conn_limit has actually been set / updated to 200
+  become_user: "{{ pg_user }}"
+  become: True
+  shell: echo "SELECT datconnlimit AS conn_limit FROM pg_database WHERE datname = '{{ db_name }}';" | psql -d postgres
+  register: result
+
+- assert:
+    that:
+      - "result.stdout_lines[-1] == '(1 row)'"
+      - "'200' == '{{ result.stdout_lines[-2] | trim }}'"
+
 - name: Cleanup test DB
   become_user: "{{ pg_user }}"
   become: True