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