Repository: ansible-collections/community.general

Issue:
community.general.django_manage fails on commands with arguments
### Summary

Since #3098 it's impossible to run django commands with arguments, like
```sh
./manage.py collectstatic --noinput
```
The whole `collectstatic --noinput` string is considered one argument now, which results in an error.

### Issue Type

Bug Report

### Component Name

django_manage

### Ansible Version

4.4.0

### Steps to Reproduce

```yaml
- name: Run collectatatic
  community.general.django_manage:
    command: collectstatic --noinput
    project_path: "{{ django_dir }}"
```

### Expected Results

I expect the command to be executed as `["./manage.py", "collectstatic", "--noinput"]`

### Actual Results

```
fatal: [example.com]: FAILED! => changed=false 
  cmd:
  - ./manage.py
  - collectstatic --noinput
  msg: |-
    stdout: Unknown command: 'collectstatic --noinput'
    Type 'manage.py help' for usage.
  
    :stderr: Unknown django command: collectstatic --noinput
```

### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
None

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
Files identified in the description:
* [`plugins/modules/web_infrastructure/django_manage.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/web_infrastructure/django_manage.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @russoz @scottanderson42 @tastychutney
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
CC @russoz 

Looks like shlex should be used to split up the command into a list.
@314eter thanks for reporting this issue! You are absolutely correct, and I have already added a test for that case. 

Will be working on it in the next couple of days. OTOH, the module has got some smarts in it to add `--noinput` automatically to some of the commands, `collectstatic` included. That means that, as a workaround, you can use something like:
```yaml
- name: Run collectstatic
  community.general.django_manage:
    command: collectstatic
    project_path: "{{ django_dir }}"
```
And it will produce the desired output:
```
    "cmd": [
        "./manage.py",
        "collectstatic",
        "--noinput"
    ],
```
@russoz that works for the stock django management commands and global options, but not for all (it's currently failing for me on `check --deploy --fail-level=WARNING`, and will also not work for custom management commands with extra params/options.

Will be great to see the command-as-a-string being split into a list.

Resolving patch:
diff --git a/plugins/modules/web_infrastructure/django_manage.py b/plugins/modules/web_infrastructure/django_manage.py
--- a/plugins/modules/web_infrastructure/django_manage.py
+++ b/plugins/modules/web_infrastructure/django_manage.py
@@ -158,6 +158,7 @@
 
 import os
 import sys
+import shlex
 
 from ansible.module_utils.basic import AnsibleModule
 
@@ -273,61 +274,62 @@ def main():
         ),
     )
 
-    command = module.params['command']
+    command_split = shlex.split(module.params['command'])
+    command_bin = command_split[0]
     project_path = module.params['project_path']
     virtualenv = module.params['virtualenv']
 
     for param in specific_params:
         value = module.params[param]
-        if value and param not in command_allowed_param_map[command]:
-            module.fail_json(msg='%s param is incompatible with command=%s' % (param, command))
+        if value and param not in command_allowed_param_map[command_bin]:
+            module.fail_json(msg='%s param is incompatible with command=%s' % (param, command_bin))
 
-    for param in command_required_param_map.get(command, ()):
+    for param in command_required_param_map.get(command_bin, ()):
         if not module.params[param]:
-            module.fail_json(msg='%s param is required for command=%s' % (param, command))
+            module.fail_json(msg='%s param is required for command=%s' % (param, command_bin))
 
     _ensure_virtualenv(module)
 
-    cmd = ["./manage.py", command]
+    run_cmd_args = ["./manage.py"] + command_split
 
-    if command in noinput_commands:
-        cmd.append("--noinput")
+    if command_bin in noinput_commands and '--noinput' not in command_split:
+        run_cmd_args.append("--noinput")
 
     for param in general_params:
         if module.params[param]:
-            cmd.append('--%s=%s' % (param, module.params[param]))
+            run_cmd_args.append('--%s=%s' % (param, module.params[param]))
 
     for param in specific_boolean_params:
         if module.params[param]:
-            cmd.append('--%s' % param)
+            run_cmd_args.append('--%s' % param)
 
     # these params always get tacked on the end of the command
     for param in end_of_command_params:
         if module.params[param]:
-            cmd.append(module.params[param])
+            run_cmd_args.append(module.params[param])
 
-    rc, out, err = module.run_command(cmd, cwd=project_path)
+    rc, out, err = module.run_command(run_cmd_args, cwd=project_path)
     if rc != 0:
-        if command == 'createcachetable' and 'table' in err and 'already exists' in err:
+        if command_bin == '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)
+                _fail(module, run_cmd_args, err, "Unknown django command: %s" % command_bin)
+            _fail(module, run_cmd_args, out, err, path=os.environ["PATH"], syspath=sys.path)
 
     changed = False
 
     lines = out.split('\n')
-    filt = globals().get(command + "_filter_output", None)
+    filt = globals().get(command_bin + "_filter_output", None)
     if filt:
         filtered_output = list(filter(filt, lines))
         if len(filtered_output):
             changed = True
-    check_changed = globals().get("{0}_check_changed".format(command), None)
+    check_changed = globals().get("{0}_check_changed".format(command_bin), None)
     if check_changed:
         changed = check_changed(out)
 
-    module.exit_json(changed=changed, out=out, cmd=cmd, app_path=project_path, project_path=project_path,
+    module.exit_json(changed=changed, out=out, cmd=run_cmd_args, app_path=project_path, project_path=project_path,
                      virtualenv=virtualenv, settings=module.params['settings'], pythonpath=module.params['pythonpath'])
 
 


Test patch:
diff --git a/tests/integration/targets/django_manage/files/base_test/simple_project/p1/p1/settings.py b/tests/integration/targets/django_manage/files/base_test/simple_project/p1/p1/settings.py
--- a/tests/integration/targets/django_manage/files/base_test/simple_project/p1/p1/settings.py
+++ b/tests/integration/targets/django_manage/files/base_test/simple_project/p1/p1/settings.py
@@ -121,3 +121,4 @@
 # https://docs.djangoproject.com/en/3.1/howto/static-files/
 
 STATIC_URL = '/static/'
+STATIC_ROOT = '/tmp/django-static'
diff --git a/tests/integration/targets/django_manage/tasks/main.yaml b/tests/integration/targets/django_manage/tasks/main.yaml
--- a/tests/integration/targets/django_manage/tasks/main.yaml
+++ b/tests/integration/targets/django_manage/tasks/main.yaml
@@ -48,3 +48,9 @@
     pythonpath: "{{ tmp_django_root.path }}/1045-single-app-project/"
     command: check
     virtualenv: "{{ tmp_django_root.path }}/venv"
+
+- name: Run collectstatic --noinput on simple project
+  community.general.django_manage:
+    project_path: "{{ tmp_django_root.path }}/simple_project/p1"
+    command: collectstatic --noinput
+    virtualenv: "{{ tmp_django_root.path }}/venv"