Repository: ansible-collections/community.general

Issue:
Unable to parse virtualbox inventory file "'str' object does not support item assignment"
### Summary

I'm unable to get the virtualbox inventory using the `community.general.virtualbox` module.


### Issue Type

Bug Report

### Component Name

virtualbox

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.13.4]
  config file = /home/etienne/playbook/ansible.cfg
  configured module search path = ['/home/etienne/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/etienne/.local/lib/python3.8/site-packages/ansible
  ansible collection location = /home/etienne/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.8.10 (default, Jun 22 2022, 20:18:18) [GCC 9.4.0]
  jinja version = 3.1.2
  libyaml = True

```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general
# /usr/lib/python3/dist-packages/ansible_collections
Collection        Version
----------------- -------
community.general 4.8.3  

# /home/etienne/.ansible/collections/ansible_collections
Collection        Version
----------------- -------
community.general 5.6.0  
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed
  ANSIBLE_FORCE_COLOR(/home/etienne/playbook/ansible.cfg) = True
  INVENTORY_ENABLED(/home/etienne/playbook/ansible.cfg) = ['host_list', 'script', 'auto', 'yaml', 'ini', 'toml', 'virtualbox']
```


### OS / Environment

Ubuntu 20.04
Linux P50 5.15.0-48-generic #54~20.04.1-Ubuntu SMP Thu Sep 1 16:17:26 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

### Steps to Reproduce

When I try to load a VIrtualBox inventory with the following example:

vbox.yml
```yaml
---
plugin: virtualbox
```

Doing the following command:
```bash
ansible all -i vbox.yml --list -vvv
```

I get:

> ansible [core 2.13.4]
  config file = /home/etienne/playbook/ansible.cfg
  configured module search path = ['/home/etienne/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/etienne/.local/lib/python3.8/site-packages/ansible
  ansible collection location = /home/etienne/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.8.10 (default, Jun 22 2022, 20:18:18) [GCC 9.4.0]
  jinja version = 3.1.2
  libyaml = True
Using /home/etienne/playbook/ansible.cfg as config file
redirecting (type: inventory) ansible.builtin.virtualbox to community.general.virtualbox
host_list declined parsing /home/etienne/playbook/vbox.yml as it did not pass its verify_file() method
script declined parsing /home/etienne/playbook/vbox.yml as it did not pass its verify_file() method
redirecting (type: inventory) ansible.builtin.virtualbox to community.general.virtualbox
Using inventory plugin 'ansible_collections.community.general.plugins.inventory.virtualbox' to process inventory source '/home/etienne/playbook/vbox.yml'
toml declined parsing /home/etienne/playbook/vbox.yml as it did not pass its verify_file() method
[WARNING]:  * Failed to parse /home/etienne/playbook/vbox.yml with auto plugin: 'str' object does not support item assignment
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/inventory/manager.py", line 290, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/plugins/inventory/auto.py", line 59, in parse
    plugin.parse(inventory, loader, path, cache=cache)
  File "/home/etienne/.ansible/collections/ansible_collections/community/general/plugins/inventory/virtualbox.py", line 281, in parse
    cacheable_results = self._populate_from_source(source_data, using_current_cache)
  File "/home/etienne/.ansible/collections/ansible_collections/community/general/plugins/inventory/virtualbox.py", line 192, in _populate_from_source
    hostvars[current_host][prevkey][pref_k] = v
[WARNING]:  * Failed to parse /home/etienne/playbook/vbox.yml with yaml plugin: Plugin configuration YAML file, not YAML inventory
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/inventory/manager.py", line 290, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/plugins/inventory/yaml.py", line 114, in parse
    raise AnsibleParserError('Plugin configuration YAML file, not YAML inventory')
[WARNING]:  * Failed to parse /home/etienne/playbook/vbox.yml with ini plugin: Invalid host pattern '---' supplied, '---' is normally a sign this is a YAML file.
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/inventory/manager.py", line 290, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/plugins/inventory/ini.py", line 136, in parse
    raise AnsibleParserError(e)
[WARNING]:  * Failed to parse /home/etienne/playbook/vbox.yml with ansible_collections.community.general.plugins.inventory.virtualbox plugin: 'str' object does not support item assignment
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/inventory/manager.py", line 290, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/home/etienne/.ansible/collections/ansible_collections/community/general/plugins/inventory/virtualbox.py", line 281, in parse
    cacheable_results = self._populate_from_source(source_data, using_current_cache)
  File "/home/etienne/.ansible/collections/ansible_collections/community/general/plugins/inventory/virtualbox.py", line 192, in _populate_from_source
    hostvars[current_host][prevkey][pref_k] = v
[WARNING]: Unable to parse /home/etienne/playbook/vbox.yml as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
  hosts (0):


Where:
> [WARNING]:  * Failed to parse /home/etienne/playbook/vbox.yml with ansible_collections.community.general.plugins.inventory.virtualbox plugin: 'str' object does not support item assignment
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/inventory/manager.py", line 290, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/home/etienne/.ansible/collections/ansible_collections/community/general/plugins/inventory/virtualbox.py", line 281, in parse
    cacheable_results = self._populate_from_source(source_data, using_current_cache)
  File "/home/etienne/.ansible/collections/ansible_collections/community/general/plugins/inventory/virtualbox.py", line 192, in _populate_from_source
    hostvars[current_host][prevkey][pref_k] = v

is what we want.

### Expected Results

I would expect to get a valid VirtualBox inventory.

### Actual Results

```console (paste below)
$ ansible all -i vbox.yml --list -vvvv
ansible [core 2.13.4]
  config file = /home/etienne/playbook/ansible.cfg
  configured module search path = ['/home/etienne/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/etienne/.local/lib/python3.8/site-packages/ansible
  ansible collection location = /home/etienne/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.8.10 (default, Jun 22 2022, 20:18:18) [GCC 9.4.0]
  jinja version = 3.1.2
  libyaml = True
Using /home/etienne/playbook/ansible.cfg as config file
setting up inventory plugins
redirecting (type: inventory) ansible.builtin.virtualbox to community.general.virtualbox
Loading collection community.general from /home/etienne/.ansible/collections/ansible_collections/community/general
host_list declined parsing /home/etienne/playbook/vbox.yml as it did not pass its verify_file() method
script declined parsing /home/etienne/playbook/vbox.yml as it did not pass its verify_file() method
redirecting (type: inventory) ansible.builtin.virtualbox to community.general.virtualbox
Using inventory plugin 'ansible_collections.community.general.plugins.inventory.virtualbox' to process inventory source '/home/etienne/playbook/vbox.yml'
toml declined parsing /home/etienne/playbook/vbox.yml as it did not pass its verify_file() method
[WARNING]:  * Failed to parse /home/etienne/playbook/vbox.yml with auto plugin: 'str' object does not support item assignment
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/inventory/manager.py", line 290, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/plugins/inventory/auto.py", line 59, in parse
    plugin.parse(inventory, loader, path, cache=cache)
  File "/home/etienne/.ansible/collections/ansible_collections/community/general/plugins/inventory/virtualbox.py", line 281, in parse
    cacheable_results = self._populate_from_source(source_data, using_current_cache)
  File "/home/etienne/.ansible/collections/ansible_collections/community/general/plugins/inventory/virtualbox.py", line 192, in _populate_from_source
    hostvars[current_host][prevkey][pref_k] = v
[WARNING]:  * Failed to parse /home/etienne/playbook/vbox.yml with yaml plugin: Plugin configuration YAML file, not YAML inventory
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/inventory/manager.py", line 290, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/plugins/inventory/yaml.py", line 114, in parse
    raise AnsibleParserError('Plugin configuration YAML file, not YAML inventory')
[WARNING]:  * Failed to parse /home/etienne/playbook/vbox.yml with ini plugin: Invalid host pattern '---' supplied, '---' is normally a sign this is a YAML file.
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/inventory/manager.py", line 290, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/plugins/inventory/ini.py", line 136, in parse
    raise AnsibleParserError(e)
[WARNING]:  * Failed to parse /home/etienne/playbook/vbox.yml with ansible_collections.community.general.plugins.inventory.virtualbox plugin: 'str' object does not support item assignment
  File "/home/etienne/.local/lib/python3.8/site-packages/ansible/inventory/manager.py", line 290, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/home/etienne/.ansible/collections/ansible_collections/community/general/plugins/inventory/virtualbox.py", line 281, in parse
    cacheable_results = self._populate_from_source(source_data, using_current_cache)
  File "/home/etienne/.ansible/collections/ansible_collections/community/general/plugins/inventory/virtualbox.py", line 192, in _populate_from_source
    hostvars[current_host][prevkey][pref_k] = v
[WARNING]: Unable to parse /home/etienne/playbook/vbox.yml as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
  hosts (0):

```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/inventory/virtualbox.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/inventory/virtualbox.py)

If these files are incorrect, 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/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
@ansibullbot !component +plugins/inventory/virtualbox.py
Just ran into this as well. I'm running VirtualBox version 6.1.38 on MacOS 12.6.

I did some sleuthing of the code with a bunch of print statements and found that the logic to parse the `VBoxManage showvminfo` data isn't handling some of the nested data correctly. Here's the `showvminfo` that's blowing up for me:

```
Recording screens:           1
 Screen 0:
    Enabled:                 yes
    ID:                      0
    Record video:            yes
    Record audio:            no
    Destination:             File
    File:                    /Users/<me>/VirtualBox VMs/<vbox instance>/<vbox-instance>-screen0.webm
    Options:                 vc_enabled=true,ac_enabled=false,ac_profile=med
    Video dimensions:        1024x768
    Video rate:              512kbps
    Video FPS:               25fps
```

It seems since `Recording screens` has both a value and a nested dictionary, it's blowing up on line 192: https://github.com/ansible-collections/community.general/blob/main/plugins/inventory/virtualbox.py#L192

In addition, it seems it's a double nested dictionary, so not sure if that's being handled correctly either. As a temporary fix, I just edited `~/.ansible/collections/ansible_collections/community/general/plugins/inventory/virtualbox.py` file, line 190 to this:

```
                    if prevkey not in hostvars[current_host] or not isinstance(hostvars[current_host][prevkey], dict):
```

It doesn't handle the double nested information correctly, but it at least doesn't cause the whole thing to blow up.
@basicdays want to create a PR for that (with a [changelog fragment](https://docs.ansible.com/ansible/devel/community/collection_development_process.html#creating-a-changelog-fragment))? While this isn't a great long-term fix (as you noted), at least it makes it restores basic functionality short-term :)
> @basicdays want to create a PR for that (with a [changelog fragment](https://docs.ansible.com/ansible/devel/community/collection_development_process.html#creating-a-changelog-fragment))? While this isn't a great long-term fix (as you noted), at least it makes it restores basic functionality short-term :)

@felixfontein Yeah I can do that, I was actually just looking into the process to do a PR for this.

Resolving patch:
diff --git a/plugins/inventory/virtualbox.py b/plugins/inventory/virtualbox.py
--- a/plugins/inventory/virtualbox.py
+++ b/plugins/inventory/virtualbox.py
@@ -186,10 +186,13 @@ def _populate_from_source(self, source_data, using_current_cache=False):
             else:
                 # found vars, accumulate in hostvars for clean inventory set
                 pref_k = 'vbox_' + k.strip().replace(' ', '_')
-                if k.startswith(' '):
-                    if prevkey not in hostvars[current_host]:
+                leading_spaces = len(k) - len(k.lstrip(' '))
+                if 0 < leading_spaces <= 2:
+                    if prevkey not in hostvars[current_host] or not isinstance(hostvars[current_host][prevkey], dict):
                         hostvars[current_host][prevkey] = {}
                     hostvars[current_host][prevkey][pref_k] = v
+                elif leading_spaces > 2:
+                    continue
                 else:
                     if v != '':
                         hostvars[current_host][pref_k] = v


Test patch: