postgresql_db 9.96 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
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.
25
version_added: "0.6"
26 27 28 29 30 31 32 33 34 35 36 37 38
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
  port:
    description:
      - Database port to connect to.
    required: false
    default: 5432
57 58 59 60 61
  template:
    description:
      - Template used to create 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
  encoding:
    description:
      - Encoding of the database
    required: false
    default: null
72 73 74 75 76 77 78 79 80 81
  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
82 83 84 85 86 87 88
  state:
    description:
      - The database state
    required: false
    default: present
    choices: [ "present", "absent" ]
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
EXAMPLES = '''
# Create a new database with name "acme"
98
- postgresql_db: name=acme
99 100 101 102

# Create a new database with name "acme" and specific encoding and locale
# settings. If a template different from "template0" is specified, encoding
# and locale settings must match those of the template.
103
- postgresql_db: name=acme
104 105 106 107 108 109
                 encoding='UTF-8'
                 lc_collate='de_DE.UTF-8'
                 lc_ctype='de_DE.UTF-8'
                 template='template0'
'''

110 111
try:
    import psycopg2
112
    import psycopg2.extras
113 114 115 116 117
except ImportError:
    postgresqldb_found = False
else:
    postgresqldb_found = True

118 119 120 121
class NotSupportedError(Exception):
    pass


122 123 124 125
# ===========================================
# PostgreSQL module specific support methods.
#

126
def set_owner(cursor, db, owner):
127
    query = "ALTER DATABASE \"%s\" OWNER TO \"%s\"" % (db, owner)
128 129 130
    cursor.execute(query)
    return True

131 132 133 134 135 136 137
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 = """
TERAOKA Yoshinori committed
138
    SELECT rolname AS owner,
139 140
    pg_encoding_to_char(encoding) AS encoding, encoding AS encoding_id,
    datcollate AS lc_collate, datctype AS lc_ctype
TERAOKA Yoshinori committed
141
    FROM pg_database JOIN pg_roles ON pg_roles.oid = pg_database.datdba
142 143 144 145
    WHERE datname = %(db)s
    """
    cursor.execute(query, {'db':db})
    return cursor.fetchone()
146

147 148 149 150 151 152
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):
153
    if db_exists(cursor, db):
154
        query = "DROP DATABASE \"%s\"" % db
155 156 157 158
        cursor.execute(query)
        return True
    else:
        return False
159

160
def db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype):
161 162
    if not db_exists(cursor, db):
        if owner:
163
            owner = " OWNER \"%s\"" % owner
164
        if template:
165
            template = " TEMPLATE \"%s\"" % template
166 167
        if encoding:
            encoding = " ENCODING '%s'" % encoding
168 169 170 171 172 173 174
        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)
175 176 177
        cursor.execute(query)
        return True
    else:
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
        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
199

200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
def db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype):
    if not db_exists(cursor, db):
       return False
    else:
        db_info = get_db_info(cursor, db)
        if (encoding and 
            get_encoding_id(cursor, encoding) != db_info['encoding_id']):
            return False
        elif lc_collate and lc_collate != db_info['lc_collate']:
            return False
        elif lc_ctype and lc_ctype != db_info['lc_ctype']:
            return False
        elif owner and owner != db_info['owner']:
            return False
        else:
            return True

217 218 219 220 221 222 223
# ===========================================
# Module execution.
#

def main():
    module = AnsibleModule(
        argument_spec=dict(
224 225 226
            login_user=dict(default="postgres"),
            login_password=dict(default=""),
            login_host=dict(default=""),
227
            port=dict(default="5432"),
228
            db=dict(required=True, aliases=['name']),
229 230 231
            owner=dict(default=""),
            template=dict(default=""),
            encoding=dict(default=""),
232 233
            lc_collate=dict(default=""),
            lc_ctype=dict(default=""),
234
            state=dict(default="present", choices=["absent", "present"]),
235
        ),
236
        supports_check_mode = True
237 238 239 240 241 242
    )

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

    db = module.params["db"]
243
    port = module.params["port"]
244 245 246
    owner = module.params["owner"]
    template = module.params["template"]
    encoding = module.params["encoding"]
247 248
    lc_collate = module.params["lc_collate"]
    lc_ctype = module.params["lc_ctype"]
249 250
    state = module.params["state"]
    changed = False
251 252 253 254

    # 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
255
    params_map = {
256 257
        "login_host":"host",
        "login_user":"user",
258 259
        "login_password":"password",
        "port":"port"
260 261 262
    }
    kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems() 
              if k in params_map and v != '' )
263
    try:
264
        db_connection = psycopg2.connect(database="template1", **kw)
265
        # Enable autocommit so we can create databases
266 267 268 269 270 271
        if psycopg2.__version__ >= '2.4.2':
            db_connection.autocommit = True
        else:
            db_connection.set_isolation_level(psycopg2
                                              .extensions
                                              .ISOLATION_LEVEL_AUTOCOMMIT)
272 273
        cursor = db_connection.cursor(
                cursor_factory=psycopg2.extras.DictCursor)
274
    except Exception, e:
275 276 277
        module.fail_json(msg="unable to connect to database: %s" % e)

    try:
278
        if module.check_mode:
279 280 281 282 283 284
            if state == "absent":
                changed = not db_exists(cursor, db)
            elif state == "present":
                changed = not db_matches(cursor, db, owner, template, encoding,
                                         lc_collate, lc_ctype)
            module.exit_json(changed=changed,db=db)
285

286 287
        if state == "absent":
            changed = db_delete(cursor, db)
288

289
        elif state == "present":
290 291 292 293
            changed = db_create(cursor, db, owner, template, encoding,
                                lc_collate, lc_ctype)
    except NotSupportedError, e:
        module.fail_json(msg=str(e))
294
    except Exception, e:
295 296 297 298
        module.fail_json(msg="Database query failed: %s" % e)

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

299
# import module snippets
300
from ansible.module_utils.basic import *
301
main()