postgresql_db 9.01 KB
Newer Older
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
DOCUMENTATION = '''
---
module: postgresql_db
short_description: Add or remove PostgreSQL databases from a remote host.
description:
   - Add or remove PostgreSQL databases from a remote host.
version_added: "0.6"
options:
  name:
    description:
      - name of the database to add or remove
    required: true
    default: null
  login_user:
    description:
      - The username used to authenticate with
    required: false
    default: null
  login_password:
    description:
39
      - The password used to authenticate with
40 41 42 43 44 45 46 47 48 49 50 51
    required: false
    default: null
  login_host:
    description:
      - Host running the database
    required: false
    default: localhost
  owner:
    description:
      - Name of the role to set as owner of the database
    required: false
    default: null
52 53 54 55 56
  template:
    description:
      - Template used to create the database
    required: false
    default: null
57 58 59 60 61
  encoding:
    description:
      - Encoding of the database
    required: false
    default: null
62 63 64 65 66
  encoding:
    description:
      - Encoding of the database
    required: false
    default: null
67 68 69 70 71 72 73 74 75 76
  lc_collate:
    description:
      - Collation order (LC_COLLATE) to use in the database. Must match collation order of template database unless C(template0) is used as template.
    required: false
    default: null
  lc_ctype:
    description:
      - Character classification (LC_CTYPE) to use in the database (e.g. lower, upper, ...) Must match LC_CTYPE of template database unless C(template0) is used as template.
    required: false
    default: null
77 78 79 80 81 82 83
  state:
    description:
      - The database state
    required: false
    default: present
    choices: [ "present", "absent" ]
examples:
84
   - code: "postgresql_db: db=acme"
Jan-Piet Mens committed
85
     description: Create a new database with name C(acme)
86 87
   - code: "postgresql_db: db=acme encoding='UTF-8' lc_collate='de_DE.UTF-8' lc_ctype='de_DE.UTF-8' template='template0'"
     description: Create a new database with name C(acme) and specific encoding and locale settings. If a template different from C(template0) is specified, encoding and locale settings must match those of the template.
88
notes:
Jan-Piet Mens committed
89 90 91
   - The default authentication assumes that you are either logging in as or sudo'ing to the C(postgres) account on the host.
   - This module uses I(psycopg2), a Python PostgreSQL database adapter. You must ensure that psycopg2 is installed on
     the host before using this module. If the remote host is the PostgreSQL server (which is the default case), then PostgreSQL must also be installed on the remote host. For Ubuntu-based systems, install the C(postgresql), C(libpq-dev), and C(python-psycopg2) packages on the remote host before using this module.
92 93 94 95
requirements: [ psycopg2 ]
author: Lorin Hochstein
'''

96 97
try:
    import psycopg2
98
    import psycopg2.extras
99 100 101 102 103
except ImportError:
    postgresqldb_found = False
else:
    postgresqldb_found = True

104 105 106 107
class NotSupportedError(Exception):
    pass


108 109 110 111
# ===========================================
# PostgreSQL module specific support methods.
#

112
def set_owner(cursor, db, owner):
113
    query = "ALTER DATABASE \"%s\" OWNER TO \"%s\"" % (db, owner)
114 115 116
    cursor.execute(query)
    return True

117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
def get_encoding_id(cursor, encoding):
    query = "SELECT pg_char_to_encoding(%(encoding)s) AS encoding_id;"
    cursor.execute(query, {'encoding': encoding})
    return cursor.fetchone()['encoding_id']

def get_db_info(cursor, db):
    query = """
    SELECT usename AS owner,
    pg_encoding_to_char(encoding) AS encoding, encoding AS encoding_id,
    datcollate AS lc_collate, datctype AS lc_ctype
    FROM pg_database JOIN pg_user ON pg_user.usesysid = pg_database.datdba
    WHERE datname = %(db)s
    """
    cursor.execute(query, {'db':db})
    return cursor.fetchone()
132

133 134 135 136 137 138
def db_exists(cursor, db):
    query = "SELECT * FROM pg_database WHERE datname=%(db)s"
    cursor.execute(query, {'db': db})
    return cursor.rowcount == 1

def db_delete(cursor, db):
139
    if db_exists(cursor, db):
140
        query = "DROP DATABASE \"%s\"" % db
141 142 143 144
        cursor.execute(query)
        return True
    else:
        return False
145

146
def db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype):
147 148
    if not db_exists(cursor, db):
        if owner:
149
            owner = " OWNER \"%s\"" % owner
150
        if template:
151
            template = " TEMPLATE \"%s\"" % template
152 153
        if encoding:
            encoding = " ENCODING '%s'" % encoding
154 155 156 157 158 159 160
        if lc_collate:
            lc_collate = " LC_COLLATE '%s'" % lc_collate
        if lc_ctype:
            lc_ctype = " LC_CTYPE '%s'" % lc_ctype
        query = 'CREATE DATABASE "%s"%s%s%s%s%s' % (db, owner,
                                                    template, encoding,
                                                    lc_collate, lc_ctype)
161 162 163
        cursor.execute(query)
        return True
    else:
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
        db_info = get_db_info(cursor, db)
        if (encoding and 
            get_encoding_id(cursor, encoding) != db_info['encoding_id']):
            raise NotSupportedError(
                'Changing database encoding is not supported. '
                'Current encoding: %s' % db_info['encoding']
            )
        elif lc_collate and lc_collate != db_info['lc_collate']:
            raise NotSupportedError(
                'Changing LC_COLLATE is not supported. '
                'Current LC_COLLATE: %s' % db_info['lc_collate']
            )
        elif lc_ctype and lc_ctype != db_info['lc_ctype']:
            raise NotSupportedError(
                '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
185 186 187 188 189 190 191 192

# ===========================================
# Module execution.
#

def main():
    module = AnsibleModule(
        argument_spec=dict(
193 194 195
            login_user=dict(default="postgres"),
            login_password=dict(default=""),
            login_host=dict(default=""),
196
            port=dict(default="5432"),
197
            db=dict(required=True, aliases=['name']),
198 199 200
            owner=dict(default=""),
            template=dict(default=""),
            encoding=dict(default=""),
201 202
            lc_collate=dict(default=""),
            lc_ctype=dict(default=""),
203
            state=dict(default="present", choices=["absent", "present"]),
204
        ),
205
        supports_check_mode = True
206 207 208 209 210 211
    )

    if not postgresqldb_found:
        module.fail_json(msg="the python psycopg2 module is required")

    db = module.params["db"]
212
    port = module.params["port"]
213 214 215
    owner = module.params["owner"]
    template = module.params["template"]
    encoding = module.params["encoding"]
216 217
    lc_collate = module.params["lc_collate"]
    lc_ctype = module.params["lc_ctype"]
218 219
    state = module.params["state"]
    changed = False
220 221 222 223

    # To use defaults values, keyword arguments must be absent, so 
    # check which values are empty and don't include in the **kw
    # dictionary
Michael DeHaan committed
224
    params_map = {
225 226
        "login_host":"host",
        "login_user":"user",
227 228
        "login_password":"password",
        "port":"port"
229 230 231
    }
    kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems() 
              if k in params_map and v != '' )
232
    try:
233
        db_connection = psycopg2.connect(database="template1", **kw)
234
        # Enable autocommit so we can create databases
235 236 237 238 239 240
        if psycopg2.__version__ >= '2.4.2':
            db_connection.autocommit = True
        else:
            db_connection.set_isolation_level(psycopg2
                                              .extensions
                                              .ISOLATION_LEVEL_AUTOCOMMIT)
241 242
        cursor = db_connection.cursor(
                cursor_factory=psycopg2.extras.DictCursor)
243
    except Exception, e:
244 245 246
        module.fail_json(msg="unable to connect to database: %s" % e)

    try:
247 248 249
        if module.check_mode:
            module.exit_json(changed=True,db=db)

250 251
        if state == "absent":
            changed = db_delete(cursor, db)
252

253
        elif state == "present":
254 255 256 257
            changed = db_create(cursor, db, owner, template, encoding,
                                lc_collate, lc_ctype)
    except NotSupportedError, e:
        module.fail_json(msg=str(e))
258
    except Exception, e:
259 260 261 262 263 264 265
        module.fail_json(msg="Database query failed: %s" % e)

    module.exit_json(changed=changed, db=db)

# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()