summaryrefslogtreecommitdiff
path: root/web_infrastructure/django_manage.py
blob: f334f3989b29e1c329dd9c8f4cf0a6ad7dc56c30 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) 2013, Scott Anderson <scottanderson42@gmail.com>
#
# 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/>.
#

DOCUMENTATION = '''
---
module: django_manage
short_description: Manages a Django application.
description:
     - Manages a Django application using the I(manage.py) application frontend to I(django-admin). With the I(virtualenv) parameter, all management commands will be executed by the given I(virtualenv) installation.
version_added: "1.1"
options:
  command:
    choices: [ 'cleanup', 'collectstatic', 'flush', 'loaddata', 'migrate', 'runfcgi', 'syncdb', 'test', 'validate', ]
    description:
      - The name of the Django management command to run. Built in commands are cleanup, collectstatic, flush, loaddata, migrate, runfcgi, syncdb, test, and validate.
      - Other commands can be entered, but will fail if they're unknown to Django.  Other commands that may prompt for user input should be run with the I(--noinput) flag.
    required: true
  app_path:
    description:
      - The path to the root of the Django application where B(manage.py) lives.
    required: true
  settings:
    description:
      - The Python path to the application's settings module, such as 'myapp.settings'.
    required: false
  pythonpath:
    description:
      - A directory to add to the Python path. Typically used to include the settings module if it is located external to the application directory.
    required: false
  virtualenv:
    description:
      - An optional path to a I(virtualenv) installation to use while running the manage application.
    required: false
  apps:
    description:
      - A list of space-delimited apps to target. Used by the 'test' command.
    required: false
  cache_table:
    description:
      - The name of the table used for database-backed caching. Used by the 'createcachetable' command.
    required: false
  database:
    description:
      - The database to target. Used by the 'createcachetable', 'flush', 'loaddata', and 'syncdb' commands.
    required: false
  failfast:
    description:
      - Fail the command immediately if a test fails. Used by the 'test' command.
    required: false
    default: "no"
    choices: [ "yes", "no" ]
  fixtures:
    description:
      - A space-delimited list of fixture file names to load in the database. B(Required) by the 'loaddata' command.
    required: false
  skip:
    description:
     - Will skip over out-of-order missing migrations, you can only use this parameter with I(migrate)
    required: false
    version_added: "1.3"
  merge:
    description:
     - Will run out-of-order or missing migrations as they are not rollback migrations, you can only use this parameter with 'migrate' command
    required: false
    version_added: "1.3"
  link:
    description:
     - Will create links to the files instead of copying them, you can only use this parameter with 'collectstatic' command
    required: false
    version_added: "1.3"
notes:
   - I(virtualenv) (U(http://www.virtualenv.org)) must be installed on the remote host if the virtualenv parameter is specified.
   - This module will create a virtualenv if the virtualenv parameter is specified and a virtualenv does not already exist at the given location.
   - This module assumes English error messages for the 'createcachetable' command to detect table existence, unfortunately.
   - To be able to use the migrate command with django versions < 1.7, you must have south installed and added as an app in your settings.
   - To be able to use the collectstatic command, you must have enabled staticfiles in your settings.
   - As of ansible 2.x, your I(manage.py) application must be executable (rwxr-xr-x), and must have a valid I(shebang), i.e. "#!/usr/bin/env python", for invoking the appropriate Python interpreter.
requirements: [ "virtualenv", "django" ]
author: "Scott Anderson (@tastychutney)"
'''

EXAMPLES = """
# Run cleanup on the application installed in 'django_dir'.
- django_manage:
    command: cleanup
    app_path: "{{ django_dir }}"

# Load the initial_data fixture into the application
- django_manage:
    command: loaddata
    app_path: "{{ django_dir }}"
    fixtures: "{{ initial_data }}"

# Run syncdb on the application
- django_manage:
    command: syncdb
    app_path: "{{ django_dir }}"
    settings: "{{ settings_app_name }}"
    pythonpath: "{{ settings_dir }}"
    virtualenv: "{{ virtualenv_dir }}"

# Run the SmokeTest test case from the main app. Useful for testing deploys.
- django_manage:
    command: test
    app_path: "{{ django_dir }}"
    apps: main.SmokeTest

# Create an initial superuser.
- django_manage:
    command: "createsuperuser --noinput --username=admin --email=admin@example.com"
    app_path: "{{ django_dir }}"
"""


import os

def _fail(module, cmd, out, err, **kwargs):
    msg = ''
    if out:
        msg += "stdout: %s" % (out, )
    if err:
        msg += "\n:stderr: %s" % (err, )
    module.fail_json(cmd=cmd, msg=msg, **kwargs)


def _ensure_virtualenv(module):

    venv_param = module.params['virtualenv']
    if venv_param is None:
        return

    vbin = os.path.join(os.path.expanduser(venv_param), 'bin')
    activate = os.path.join(vbin, 'activate')

    if not os.path.exists(activate):
        virtualenv = module.get_bin_path('virtualenv', True)
        vcmd = '%s %s' % (virtualenv, venv_param)
        vcmd = [virtualenv, venv_param]
        rc, out_venv, err_venv = module.run_command(vcmd)
        if rc != 0:
            _fail(module, vcmd, out_venv, err_venv)

    os.environ["PATH"] = "%s:%s" % (vbin, os.environ["PATH"])
    os.environ["VIRTUAL_ENV"] = venv_param

def createcachetable_filter_output(line):
    return "Already exists" not in line

def flush_filter_output(line):
    return "Installed" in line and "Installed 0 object" not in line

def loaddata_filter_output(line):
    return "Installed" in line and "Installed 0 object" not in line

def syncdb_filter_output(line):
    return ("Creating table " in line) or ("Installed" in line and "Installed 0 object" not in line)

def migrate_filter_output(line):
    return ("Migrating forwards " in line) or ("Installed" in line and "Installed 0 object" not in line) or ("Applying" in line)

def collectstatic_filter_output(line):
    return line and "0 static files" not in line

def main():
    command_allowed_param_map = dict(
        cleanup=(),
        createcachetable=('cache_table', 'database', ),
        flush=('database', ),
        loaddata=('database', 'fixtures', ),
        syncdb=('database', ),
        test=('failfast', 'testrunner', 'liveserver', 'apps', ),
        validate=(),
        migrate=('apps', 'skip', 'merge', 'database',),
        collectstatic=('clear', 'link', ),
        )

    command_required_param_map = dict(
        loaddata=('fixtures', ),
        )

    # forces --noinput on every command that needs it
    noinput_commands = (
        'flush',
        'syncdb',
        'migrate',
        'test',
        'collectstatic',
        )

    # These params are allowed for certain commands only
    specific_params = ('apps', 'clear', 'database', 'failfast', 'fixtures', 'liveserver', 'testrunner')

    # These params are automatically added to the command if present
    general_params = ('settings', 'pythonpath', 'database',)
    specific_boolean_params = ('clear', 'failfast', 'skip', 'merge', 'link')
    end_of_command_params = ('apps', 'cache_table', 'fixtures')

    module = AnsibleModule(
        argument_spec=dict(
            command     = dict(default=None, required=True),
            app_path    = dict(default=None, required=True),
            settings    = dict(default=None, required=False),
            pythonpath  = dict(default=None, required=False, aliases=['python_path']),
            virtualenv  = dict(default=None, required=False, aliases=['virtual_env']),

            apps        = dict(default=None, required=False),
            cache_table = dict(default=None, required=False),
            clear       = dict(default=None, required=False, type='bool'),
            database    = dict(default=None, required=False),
            failfast    = dict(default='no', required=False, type='bool', aliases=['fail_fast']),
            fixtures    = dict(default=None, required=False),
            liveserver  = dict(default=None, required=False, aliases=['live_server']),
            testrunner  = dict(default=None, required=False, aliases=['test_runner']),
            skip        = dict(default=None, required=False, type='bool'),
            merge       = dict(default=None, required=False, type='bool'),
            link        = dict(default=None, required=False, type='bool'),
        ),
    )

    command = module.params['command']
    app_path = os.path.expanduser(module.params['app_path'])
    virtualenv = module.params['virtualenv']

    for param in specific_params:
        value = module.params[param]
        if param in specific_boolean_params:
            value = module.boolean(value)
        if value and param not in command_allowed_param_map[command]:
            module.fail_json(msg='%s param is incompatible with command=%s' % (param, command))

    for param in command_required_param_map.get(command, ()):
        if not module.params[param]:
            module.fail_json(msg='%s param is required for command=%s' % (param, command))

    _ensure_virtualenv(module)

    cmd = "./manage.py %s" % (command, )

    if command in noinput_commands:
        cmd = '%s --noinput' % cmd

    for param in general_params:
        if module.params[param]:
            cmd = '%s --%s=%s' % (cmd, param, module.params[param])

    for param in specific_boolean_params:
        if module.boolean(module.params[param]):
            cmd = '%s --%s' % (cmd, param)

    # these params always get tacked on the end of the command
    for param in end_of_command_params:
        if module.params[param]:
            cmd = '%s %s' % (cmd, module.params[param])

    rc, out, err = module.run_command(cmd, cwd=os.path.expanduser(app_path))
    if rc != 0:
        if command == 'createcachetable' and 'table' in err and 'already exists' in err:
            out = 'Already exists.'
        else:
            if "Unknown command:" in err:
                _fail(module, cmd, err, "Unknown django command: %s" % command)
            _fail(module, cmd, out, err, path=os.environ["PATH"], syspath=sys.path)

    changed = False

    lines = out.split('\n')
    filt = globals().get(command + "_filter_output", None)
    if filt:
        filtered_output = filter(filt, lines)
        if len(filtered_output):
            changed = filtered_output

    module.exit_json(changed=changed, out=out, cmd=cmd, app_path=app_path, virtualenv=virtualenv,
                     settings=module.params['settings'], pythonpath=module.params['pythonpath'])

# import module snippets
from ansible.module_utils.basic import *

main()