1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# (c) 2013, Scott Anderson <scottanderson42@gmail.com>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11DOCUMENTATION = '''
12---
13module: django_manage
14short_description: Manages a Django application.
15description:
16    - Manages a Django application using the C(manage.py) application frontend to C(django-admin). With the
17      C(virtualenv) parameter, all management commands will be executed by the given C(virtualenv) installation.
18options:
19  command:
20    description:
21      - The name of the Django management command to run. Built in commands are C(cleanup), C(collectstatic),
22        C(flush), C(loaddata), C(migrate), C(syncdb), C(test), and C(validate).
23      - Other commands can be entered, but will fail if they're unknown to Django.  Other commands that may
24        prompt for user input should be run with the C(--noinput) flag.
25      - The module will perform some basic parameter validation (when applicable) to the commands C(cleanup),
26        C(collectstatic), C(createcachetable), C(flush), C(loaddata), C(migrate), C(syncdb), C(test), and C(validate).
27    type: str
28    required: true
29  project_path:
30    description:
31      - The path to the root of the Django application where B(manage.py) lives.
32    type: path
33    required: true
34    aliases: [app_path, chdir]
35  settings:
36    description:
37      - The Python path to the application's settings module, such as C(myapp.settings).
38    type: path
39    required: false
40  pythonpath:
41    description:
42      - A directory to add to the Python path. Typically used to include the settings module if it is located
43        external to the application directory.
44    type: path
45    required: false
46    aliases: [python_path]
47  virtualenv:
48    description:
49      - An optional path to a I(virtualenv) installation to use while running the manage application.
50    type: path
51    aliases: [virtual_env]
52  apps:
53    description:
54      - A list of space-delimited apps to target. Used by the C(test) command.
55    type: str
56    required: false
57  cache_table:
58    description:
59      - The name of the table used for database-backed caching. Used by the C(createcachetable) command.
60    type: str
61    required: false
62  clear:
63    description:
64      - Clear the existing files before trying to copy or link the original file.
65      - Used only with the C(collectstatic) command. The C(--noinput) argument will be added automatically.
66    required: false
67    default: no
68    type: bool
69  database:
70    description:
71      - The database to target. Used by the C(createcachetable), C(flush), C(loaddata), C(syncdb),
72        and C(migrate) commands.
73    type: str
74    required: false
75  failfast:
76    description:
77      - Fail the command immediately if a test fails. Used by the C(test) command.
78    required: false
79    default: false
80    type: bool
81    aliases: [fail_fast]
82  fixtures:
83    description:
84      - A space-delimited list of fixture file names to load in the database. B(Required) by the C(loaddata) command.
85    type: str
86    required: false
87  skip:
88    description:
89     - Will skip over out-of-order missing migrations, you can only use this parameter with C(migrate) command.
90    required: false
91    type: bool
92  merge:
93    description:
94     - Will run out-of-order or missing migrations as they are not rollback migrations, you can only use this
95       parameter with C(migrate) command.
96    required: false
97    type: bool
98  link:
99    description:
100     - Will create links to the files instead of copying them, you can only use this parameter with
101       C(collectstatic) command.
102    required: false
103    type: bool
104  testrunner:
105    description:
106      - "From the Django docs: Controls the test runner class that is used to execute tests."
107      - This parameter is passed as-is to C(manage.py).
108    type: str
109    required: false
110    aliases: [test_runner]
111notes:
112  - C(virtualenv) (U(http://www.virtualenv.org)) must be installed on the remote host if the I(virtualenv) parameter
113    is specified.
114  - This module will create a virtualenv if the I(virtualenv) parameter is specified and a virtual environment does not already
115    exist at the given location.
116  - This module assumes English error messages for the C(createcachetable) command to detect table existence,
117    unfortunately.
118  - To be able to use the C(migrate) command with django versions < 1.7, you must have C(south) installed and added
119    as an app in your settings.
120  - To be able to use the C(collectstatic) command, you must have enabled staticfiles in your settings.
121  - Your C(manage.py) application must be executable (rwxr-xr-x), and must have a valid shebang,
122    i.e. C(#!/usr/bin/env python), for invoking the appropriate Python interpreter.
123requirements: [ "virtualenv", "django" ]
124author: "Scott Anderson (@tastychutney)"
125'''
126
127EXAMPLES = """
128- name: Run cleanup on the application installed in django_dir
129  community.general.django_manage:
130    command: cleanup
131    project_path: "{{ django_dir }}"
132
133- name: Load the initial_data fixture into the application
134  community.general.django_manage:
135    command: loaddata
136    project_path: "{{ django_dir }}"
137    fixtures: "{{ initial_data }}"
138
139- name: Run syncdb on the application
140  community.general.django_manage:
141    command: syncdb
142    project_path: "{{ django_dir }}"
143    settings: "{{ settings_app_name }}"
144    pythonpath: "{{ settings_dir }}"
145    virtualenv: "{{ virtualenv_dir }}"
146
147- name: Run the SmokeTest test case from the main app. Useful for testing deploys
148  community.general.django_manage:
149    command: test
150    project_path: "{{ django_dir }}"
151    apps: main.SmokeTest
152
153- name: Create an initial superuser
154  community.general.django_manage:
155    command: "createsuperuser --noinput --username=admin --email=admin@example.com"
156    project_path: "{{ django_dir }}"
157"""
158
159import os
160import sys
161import shlex
162
163from ansible.module_utils.basic import AnsibleModule
164
165
166def _fail(module, cmd, out, err, **kwargs):
167    msg = ''
168    if out:
169        msg += "stdout: %s" % (out, )
170    if err:
171        msg += "\n:stderr: %s" % (err, )
172    module.fail_json(cmd=cmd, msg=msg, **kwargs)
173
174
175def _ensure_virtualenv(module):
176
177    venv_param = module.params['virtualenv']
178    if venv_param is None:
179        return
180
181    vbin = os.path.join(venv_param, 'bin')
182    activate = os.path.join(vbin, 'activate')
183
184    if not os.path.exists(activate):
185        virtualenv = module.get_bin_path('virtualenv', True)
186        vcmd = [virtualenv, venv_param]
187        rc, out_venv, err_venv = module.run_command(vcmd)
188        if rc != 0:
189            _fail(module, vcmd, out_venv, err_venv)
190
191    os.environ["PATH"] = "%s:%s" % (vbin, os.environ["PATH"])
192    os.environ["VIRTUAL_ENV"] = venv_param
193
194
195def createcachetable_check_changed(output):
196    return "already exists" not in output
197
198
199def flush_filter_output(line):
200    return "Installed" in line and "Installed 0 object" not in line
201
202
203def loaddata_filter_output(line):
204    return "Installed" in line and "Installed 0 object" not in line
205
206
207def syncdb_filter_output(line):
208    return ("Creating table " in line) \
209        or ("Installed" in line and "Installed 0 object" not in line)
210
211
212def migrate_filter_output(line):
213    return ("Migrating forwards " in line) \
214        or ("Installed" in line and "Installed 0 object" not in line) \
215        or ("Applying" in line)
216
217
218def collectstatic_filter_output(line):
219    return line and "0 static files" not in line
220
221
222def main():
223    command_allowed_param_map = dict(
224        cleanup=(),
225        createcachetable=('cache_table', 'database', ),
226        flush=('database', ),
227        loaddata=('database', 'fixtures', ),
228        syncdb=('database', ),
229        test=('failfast', 'testrunner', 'apps', ),
230        validate=(),
231        migrate=('apps', 'skip', 'merge', 'database',),
232        collectstatic=('clear', 'link', ),
233    )
234
235    command_required_param_map = dict(
236        loaddata=('fixtures', ),
237    )
238
239    # forces --noinput on every command that needs it
240    noinput_commands = (
241        'flush',
242        'syncdb',
243        'migrate',
244        'test',
245        'collectstatic',
246    )
247
248    # These params are allowed for certain commands only
249    specific_params = ('apps', 'clear', 'database', 'failfast', 'fixtures', 'testrunner')
250
251    # These params are automatically added to the command if present
252    general_params = ('settings', 'pythonpath', 'database',)
253    specific_boolean_params = ('clear', 'failfast', 'skip', 'merge', 'link')
254    end_of_command_params = ('apps', 'cache_table', 'fixtures')
255
256    module = AnsibleModule(
257        argument_spec=dict(
258            command=dict(required=True, type='str'),
259            project_path=dict(required=True, type='path', aliases=['app_path', 'chdir']),
260            settings=dict(type='path'),
261            pythonpath=dict(type='path', aliases=['python_path']),
262            virtualenv=dict(type='path', aliases=['virtual_env']),
263
264            apps=dict(),
265            cache_table=dict(type='str'),
266            clear=dict(default=False, type='bool'),
267            database=dict(type='str'),
268            failfast=dict(default=False, type='bool', aliases=['fail_fast']),
269            fixtures=dict(type='str'),
270            testrunner=dict(type='str', aliases=['test_runner']),
271            skip=dict(type='bool'),
272            merge=dict(type='bool'),
273            link=dict(type='bool'),
274        ),
275    )
276
277    command_split = shlex.split(module.params['command'])
278    command_bin = command_split[0]
279    project_path = module.params['project_path']
280    virtualenv = module.params['virtualenv']
281
282    for param in specific_params:
283        value = module.params[param]
284        if value and param not in command_allowed_param_map[command_bin]:
285            module.fail_json(msg='%s param is incompatible with command=%s' % (param, command_bin))
286
287    for param in command_required_param_map.get(command_bin, ()):
288        if not module.params[param]:
289            module.fail_json(msg='%s param is required for command=%s' % (param, command_bin))
290
291    _ensure_virtualenv(module)
292
293    run_cmd_args = ["./manage.py"] + command_split
294
295    if command_bin in noinput_commands and '--noinput' not in command_split:
296        run_cmd_args.append("--noinput")
297
298    for param in general_params:
299        if module.params[param]:
300            run_cmd_args.append('--%s=%s' % (param, module.params[param]))
301
302    for param in specific_boolean_params:
303        if module.params[param]:
304            run_cmd_args.append('--%s' % param)
305
306    # these params always get tacked on the end of the command
307    for param in end_of_command_params:
308        if module.params[param]:
309            if param in ('fixtures', 'apps'):
310                run_cmd_args.extend(shlex.split(module.params[param]))
311            else:
312                run_cmd_args.append(module.params[param])
313
314    rc, out, err = module.run_command(run_cmd_args, cwd=project_path)
315    if rc != 0:
316        if command_bin == 'createcachetable' and 'table' in err and 'already exists' in err:
317            out = 'already exists.'
318        else:
319            if "Unknown command:" in err:
320                _fail(module, run_cmd_args, err, "Unknown django command: %s" % command_bin)
321            _fail(module, run_cmd_args, out, err, path=os.environ["PATH"], syspath=sys.path)
322
323    changed = False
324
325    lines = out.split('\n')
326    filt = globals().get(command_bin + "_filter_output", None)
327    if filt:
328        filtered_output = list(filter(filt, lines))
329        if len(filtered_output):
330            changed = True
331    check_changed = globals().get("{0}_check_changed".format(command_bin), None)
332    if check_changed:
333        changed = check_changed(out)
334
335    module.exit_json(changed=changed, out=out, cmd=run_cmd_args, app_path=project_path, project_path=project_path,
336                     virtualenv=virtualenv, settings=module.params['settings'], pythonpath=module.params['pythonpath'])
337
338
339if __name__ == '__main__':
340    main()
341