We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
OpenNebula dynamic inventory plugin does not support keyed_groups
### Summary

The documentation for the OpenNebula dynamic inventory plugin [0] refers to an option to created keyed_groups (because the documentation extends it).

```
    extends_documentation_fragment:
        - constructed
```

However, this functionality is not implemented.  Even though the `InventoryModule` inherits from `Constructable`, `self._add_host_to_keyed_groups()` is never called.

[0] https://docs.ansible.com/ansible/latest/collections/community/general/opennebula_inventory.html

### Issue Type

Bug Report

### Component Name

opennebula dynamic inventory plugin

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.12.4]
  config file = /code/rafiki/ansible.cfg
  configured module search path = ['/home/vagrant/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/vagrant/.pyenv/versions/3.10.3/lib/python3.10/site-packages/ansible
  ansible collection location = /home/vagrant/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/vagrant/.pyenv/versions/3.10.3/bin/ansible
  python version = 3.10.3 (main, Apr  6 2022, 22:36:07) [GCC 11.2.0]
  jinja version = 3.1.1
  libyaml = True

```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general

# /home/vagrant/.pyenv/versions/3.10.3/lib/python3.10/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 4.7.0  

```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed
HOST_KEY_CHECKING(/code/rafiki/ansible.cfg) = False

```


### OS / Environment

```
vagrant@ubuntu-jammy:/code/rafiki$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu Jammy Jellyfish (development branch)"
```

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->

opennebula.yml:

```yaml (paste below)
plugin: community.general.opennebula
api_url: "RPC_URL"
# specify these here, or in the form username:password in ~/.one/one_auth or export ONE_AUTH=/some/other/path
#api_username:
#api_password:
keyed_groups:
  - prefix: distro
    key: ansible_distribution

```

`ansible-inventory --list -i opennebula.yml`

### Expected Results

I expected a grouping of hosts by distribution.  Note that the plugin does perform grouping by LABEL, which is seen below with the `srv` and `foo` labels/groups.

### Actual Results

```console (paste below)
```console
ansible-inventory --list -i opennebula.yml
{
    "_meta": {
        "hostvars": {
            "terraform_demo_00": {
                "INFO": "",
                "INPUTS_ORDER": "",
                "LABELS": [],
                "LOGO": "images/logos/logo.png",
                "LXD_SECURITY_PRIVILEGED": "true",
                "MEMORY_UNIT_COST": "MB",
                "SCHED_REQUIREMENTS": "ARCH=\"x86_64\"",
                "ansible_host": "192.168.11.235",
                "name": "terraform_demo_00",
                "v4_first_ip": "192.168.11.235",
                "v6_first_ip": false
            },
            "terraform_demo_01": {
                "INFO": "",
                "INPUTS_ORDER": "",
                "LABELS": [
                    "srv",
                    "foo"
                ],
                "LOGO": "images/logos/logo.png",
                "LXD_SECURITY_PRIVILEGED": "true",
                "MEMORY_UNIT_COST": "MB",
                "SCHED_REQUIREMENTS": "ARCH=\"x86_64\"",
                "ansible_host": "192.168.11.241",
                "name": "terraform_demo_01",
                "v4_first_ip": "192.168.11.241",
                "v6_first_ip": false
            }
        }
    },
    "all": {
        "children": [
            "srv",
            "foo",
            "ungrouped"
        ]
    },
    "srv": {
        "hosts": [
            "terraform_demo_01"
        ]
    },
    "foo": {
        "hosts": [
            "terraform_demo_01"
        ]
    },
    "ungrouped": {
        "hosts": [
            "terraform_demo_00"
        ]
    }
}
```
```


### Code of Conduct

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

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `plugins/inventory/opennebula.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2020, FELDSAM s.r.o. - FeldHost™ <support@feldhost.cz>
3 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4 
5 from __future__ import (absolute_import, division, print_function)
6 
7 __metaclass__ = type
8 
9 DOCUMENTATION = r'''
10     name: opennebula
11     author:
12         - Kristian Feldsam (@feldsam)
13     short_description: OpenNebula inventory source
14     version_added: "3.8.0"
15     extends_documentation_fragment:
16         - constructed
17     description:
18         - Get inventory hosts from OpenNebula cloud.
19         - Uses an YAML configuration file ending with either I(opennebula.yml) or I(opennebula.yaml)
20           to set parameter values.
21         - Uses I(api_authfile), C(~/.one/one_auth), or C(ONE_AUTH) pointing to a OpenNebula credentials file.
22     options:
23         plugin:
24             description: Token that ensures this is a source file for the 'opennebula' plugin.
25             type: string
26             required: true
27             choices: [ community.general.opennebula ]
28         api_url:
29             description:
30               - URL of the OpenNebula RPC server.
31               - It is recommended to use HTTPS so that the username/password are not
32                 transferred over the network unencrypted.
33               - If not set then the value of the C(ONE_URL) environment variable is used.
34             env:
35               - name: ONE_URL
36             required: True
37             type: string
38         api_username:
39             description:
40               - Name of the user to login into the OpenNebula RPC server. If not set
41                 then the value of the C(ONE_USERNAME) environment variable is used.
42             env:
43               - name: ONE_USERNAME
44             type: string
45         api_password:
46             description:
47               - Password or a token of the user to login into OpenNebula RPC server.
48               - If not set, the value of the C(ONE_PASSWORD) environment variable is used.
49             env:
50               - name: ONE_PASSWORD
51             required: False
52             type: string
53         api_authfile:
54             description:
55               - If both I(api_username) or I(api_password) are not set, then it will try
56                 authenticate with ONE auth file. Default path is C(~/.one/one_auth).
57               - Set environment variable C(ONE_AUTH) to override this path.
58             env:
59               - name: ONE_AUTH
60             required: False
61             type: string
62         hostname:
63             description: Field to match the hostname. Note C(v4_first_ip) corresponds to the first IPv4 found on VM.
64             type: string
65             default: v4_first_ip
66             choices:
67                 - v4_first_ip
68                 - v6_first_ip
69                 - name
70         filter_by_label:
71             description: Only return servers filtered by this label.
72             type: string
73         group_by_labels:
74             description: Create host groups by vm labels
75             type: bool
76             default: True
77 '''
78 
79 EXAMPLES = r'''
80 # inventory_opennebula.yml file in YAML format
81 # Example command line: ansible-inventory --list -i inventory_opennebula.yml
82 
83 # Pass a label filter to the API
84 plugin: community.general.opennebula
85 api_url: https://opennebula:2633/RPC2
86 filter_by_label: Cache
87 '''
88 
89 try:
90     import pyone
91 
92     HAS_PYONE = True
93 except ImportError:
94     HAS_PYONE = False
95 
96 from ansible.errors import AnsibleError
97 from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
98 from ansible.module_utils.common.text.converters import to_native
99 
100 from collections import namedtuple
101 import os
102 
103 
104 class InventoryModule(BaseInventoryPlugin, Constructable):
105     NAME = 'community.general.opennebula'
106 
107     def verify_file(self, path):
108         valid = False
109         if super(InventoryModule, self).verify_file(path):
110             if path.endswith(('opennebula.yaml', 'opennebula.yml')):
111                 valid = True
112         return valid
113 
114     def _get_connection_info(self):
115         url = self.get_option('api_url')
116         username = self.get_option('api_username')
117         password = self.get_option('api_password')
118         authfile = self.get_option('api_authfile')
119 
120         if not username and not password:
121             if authfile is None:
122                 authfile = os.path.join(os.environ.get("HOME"), ".one", "one_auth")
123             try:
124                 with open(authfile, "r") as fp:
125                     authstring = fp.read().rstrip()
126                 username, password = authstring.split(":")
127             except (OSError, IOError):
128                 raise AnsibleError("Could not find or read ONE_AUTH file at '{e}'".format(e=authfile))
129             except Exception:
130                 raise AnsibleError("Error occurs when reading ONE_AUTH file at '{e}'".format(e=authfile))
131 
132         auth_params = namedtuple('auth', ('url', 'username', 'password'))
133 
134         return auth_params(url=url, username=username, password=password)
135 
136     def _get_vm_ipv4(self, vm):
137         nic = vm.TEMPLATE.get('NIC')
138 
139         if isinstance(nic, dict):
140             nic = [nic]
141 
142         for net in nic:
143             return net['IP']
144 
145         return False
146 
147     def _get_vm_ipv6(self, vm):
148         nic = vm.TEMPLATE.get('NIC')
149 
150         if isinstance(nic, dict):
151             nic = [nic]
152 
153         for net in nic:
154             if net.get('IP6_GLOBAL'):
155                 return net['IP6_GLOBAL']
156 
157         return False
158 
159     def _get_vm_pool(self):
160         auth = self._get_connection_info()
161 
162         if not (auth.username and auth.password):
163             raise AnsibleError('API Credentials missing. Check OpenNebula inventory file.')
164         else:
165             one_client = pyone.OneServer(auth.url, session=auth.username + ':' + auth.password)
166 
167         # get hosts (VMs)
168         try:
169             vm_pool = one_client.vmpool.infoextended(-2, -1, -1, 3)
170         except Exception as e:
171             raise AnsibleError("Something happened during XML-RPC call: {e}".format(e=to_native(e)))
172 
173         return vm_pool
174 
175     def _retrieve_servers(self, label_filter=None):
176         vm_pool = self._get_vm_pool()
177 
178         result = []
179 
180         # iterate over hosts
181         for vm in vm_pool.VM:
182             server = vm.USER_TEMPLATE
183 
184             labels = []
185             if vm.USER_TEMPLATE.get('LABELS'):
186                 labels = [s for s in vm.USER_TEMPLATE.get('LABELS') if s == ',' or s == '-' or s.isalnum() or s.isspace()]
187                 labels = ''.join(labels)
188                 labels = labels.replace(' ', '_')
189                 labels = labels.replace('-', '_')
190                 labels = labels.split(',')
191 
192             # filter by label
193             if label_filter is not None:
194                 if label_filter not in labels:
195                     continue
196 
197             server['name'] = vm.NAME
198             server['LABELS'] = labels
199             server['v4_first_ip'] = self._get_vm_ipv4(vm)
200             server['v6_first_ip'] = self._get_vm_ipv6(vm)
201 
202             result.append(server)
203 
204         return result
205 
206     def _populate(self):
207         hostname_preference = self.get_option('hostname')
208         group_by_labels = self.get_option('group_by_labels')
209 
210         # Add a top group 'one'
211         self.inventory.add_group(group='all')
212 
213         filter_by_label = self.get_option('filter_by_label')
214         for server in self._retrieve_servers(filter_by_label):
215             # check for labels
216             if group_by_labels and server['LABELS']:
217                 for label in server['LABELS']:
218                     self.inventory.add_group(group=label)
219                     self.inventory.add_host(host=server['name'], group=label)
220 
221             self.inventory.add_host(host=server['name'], group='all')
222 
223             for attribute, value in server.items():
224                 self.inventory.set_variable(server['name'], attribute, value)
225 
226             if hostname_preference != 'name':
227                 self.inventory.set_variable(server['name'], 'ansible_host', server[hostname_preference])
228 
229             if server.get('SSH_PORT'):
230                 self.inventory.set_variable(server['name'], 'ansible_port', server['SSH_PORT'])
231 
232     def parse(self, inventory, loader, path, cache=True):
233         if not HAS_PYONE:
234             raise AnsibleError('OpenNebula Inventory plugin requires pyone to work!')
235 
236         super(InventoryModule, self).parse(inventory, loader, path)
237         self._read_config_data(path=path)
238 
239         self._populate()
240
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/plugins/inventory/opennebula.py b/plugins/inventory/opennebula.py
--- a/plugins/inventory/opennebula.py
+++ b/plugins/inventory/opennebula.py
@@ -206,28 +206,40 @@
     def _populate(self):
         hostname_preference = self.get_option('hostname')
         group_by_labels = self.get_option('group_by_labels')
+        strict = self.get_option('strict')
 
         # Add a top group 'one'
         self.inventory.add_group(group='all')
 
         filter_by_label = self.get_option('filter_by_label')
-        for server in self._retrieve_servers(filter_by_label):
+        servers = self._retrieve_servers(filter_by_label)
+        for server in servers:
+            hostname = server['name']
             # check for labels
             if group_by_labels and server['LABELS']:
                 for label in server['LABELS']:
                     self.inventory.add_group(group=label)
-                    self.inventory.add_host(host=server['name'], group=label)
+                    self.inventory.add_host(host=hostname, group=label)
 
-            self.inventory.add_host(host=server['name'], group='all')
+            self.inventory.add_host(host=hostname, group='all')
 
             for attribute, value in server.items():
-                self.inventory.set_variable(server['name'], attribute, value)
+                self.inventory.set_variable(hostname, attribute, value)
 
             if hostname_preference != 'name':
-                self.inventory.set_variable(server['name'], 'ansible_host', server[hostname_preference])
+                self.inventory.set_variable(hostname, 'ansible_host', server[hostname_preference])
 
             if server.get('SSH_PORT'):
-                self.inventory.set_variable(server['name'], 'ansible_port', server['SSH_PORT'])
+                self.inventory.set_variable(hostname, 'ansible_port', server['SSH_PORT'])
+
+            # handle construcable implementation: get composed variables if any
+            self._set_composite_vars(self.get_option('compose'), server, hostname, strict=strict)
+
+            # groups based on jinja conditionals get added to specific groups
+            self._add_host_to_composed_groups(self.get_option('groups'), server, hostname, strict=strict)
+
+            # groups based on variables associated with them in the inventory
+            self._add_host_to_keyed_groups(self.get_option('keyed_groups'), server, hostname, strict=strict)
 
     def parse(self, inventory, loader, path, cache=True):
         if not HAS_PYONE:

{"golden_diff": "diff --git a/plugins/inventory/opennebula.py b/plugins/inventory/opennebula.py\n--- a/plugins/inventory/opennebula.py\n+++ b/plugins/inventory/opennebula.py\n@@ -206,28 +206,40 @@\n     def _populate(self):\n         hostname_preference = self.get_option('hostname')\n         group_by_labels = self.get_option('group_by_labels')\n+        strict = self.get_option('strict')\n \n         # Add a top group 'one'\n         self.inventory.add_group(group='all')\n \n         filter_by_label = self.get_option('filter_by_label')\n-        for server in self._retrieve_servers(filter_by_label):\n+        servers = self._retrieve_servers(filter_by_label)\n+        for server in servers:\n+            hostname = server['name']\n             # check for labels\n             if group_by_labels and server['LABELS']:\n                 for label in server['LABELS']:\n                     self.inventory.add_group(group=label)\n-                    self.inventory.add_host(host=server['name'], group=label)\n+                    self.inventory.add_host(host=hostname, group=label)\n \n-            self.inventory.add_host(host=server['name'], group='all')\n+            self.inventory.add_host(host=hostname, group='all')\n \n             for attribute, value in server.items():\n-                self.inventory.set_variable(server['name'], attribute, value)\n+                self.inventory.set_variable(hostname, attribute, value)\n \n             if hostname_preference != 'name':\n-                self.inventory.set_variable(server['name'], 'ansible_host', server[hostname_preference])\n+                self.inventory.set_variable(hostname, 'ansible_host', server[hostname_preference])\n \n             if server.get('SSH_PORT'):\n-                self.inventory.set_variable(server['name'], 'ansible_port', server['SSH_PORT'])\n+                self.inventory.set_variable(hostname, 'ansible_port', server['SSH_PORT'])\n+\n+            # handle construcable implementation: get composed variables if any\n+            self._set_composite_vars(self.get_option('compose'), server, hostname, strict=strict)\n+\n+            # groups based on jinja conditionals get added to specific groups\n+            self._add_host_to_composed_groups(self.get_option('groups'), server, hostname, strict=strict)\n+\n+            # groups based on variables associated with them in the inventory\n+            self._add_host_to_keyed_groups(self.get_option('keyed_groups'), server, hostname, strict=strict)\n \n     def parse(self, inventory, loader, path, cache=True):\n         if not HAS_PYONE:\n", "issue": "OpenNebula dynamic inventory plugin does not support keyed_groups\n### Summary\r\n\r\nThe documentation for the OpenNebula dynamic inventory plugin [0] refers to an option to created keyed_groups (because the documentation extends it).\r\n\r\n```\r\n    extends_documentation_fragment:\r\n        - constructed\r\n```\r\n\r\nHowever, this functionality is not implemented.  Even though the `InventoryModule` inherits from `Constructable`, `self._add_host_to_keyed_groups()` is never called.\r\n\r\n[0] https://docs.ansible.com/ansible/latest/collections/community/general/opennebula_inventory.html\r\n\r\n### Issue Type\r\n\r\nBug Report\r\n\r\n### Component Name\r\n\r\nopennebula dynamic inventory plugin\r\n\r\n### Ansible Version\r\n\r\n```console (paste below)\r\n$ ansible --version\r\nansible [core 2.12.4]\r\n  config file = /code/rafiki/ansible.cfg\r\n  configured module search path = ['/home/vagrant/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']\r\n  ansible python module location = /home/vagrant/.pyenv/versions/3.10.3/lib/python3.10/site-packages/ansible\r\n  ansible collection location = /home/vagrant/.ansible/collections:/usr/share/ansible/collections\r\n  executable location = /home/vagrant/.pyenv/versions/3.10.3/bin/ansible\r\n  python version = 3.10.3 (main, Apr  6 2022, 22:36:07) [GCC 11.2.0]\r\n  jinja version = 3.1.1\r\n  libyaml = True\r\n\r\n```\r\n\r\n\r\n### Community.general Version\r\n\r\n```console (paste below)\r\n$ ansible-galaxy collection list community.general\r\n\r\n# /home/vagrant/.pyenv/versions/3.10.3/lib/python3.10/site-packages/ansible_collections\r\nCollection        Version\r\n----------------- -------\r\ncommunity.general 4.7.0  \r\n\r\n```\r\n\r\n\r\n### Configuration\r\n\r\n```console (paste below)\r\n$ ansible-config dump --only-changed\r\nHOST_KEY_CHECKING(/code/rafiki/ansible.cfg) = False\r\n\r\n```\r\n\r\n\r\n### OS / Environment\r\n\r\n```\r\nvagrant@ubuntu-jammy:/code/rafiki$ cat /etc/lsb-release \r\nDISTRIB_ID=Ubuntu\r\nDISTRIB_RELEASE=22.04\r\nDISTRIB_CODENAME=jammy\r\nDISTRIB_DESCRIPTION=\"Ubuntu Jammy Jellyfish (development branch)\"\r\n```\r\n\r\n### Steps to Reproduce\r\n\r\n<!--- Paste example playbooks or commands between quotes below -->\r\n\r\nopennebula.yml:\r\n\r\n```yaml (paste below)\r\nplugin: community.general.opennebula\r\napi_url: \"RPC_URL\"\r\n# specify these here, or in the form username:password in ~/.one/one_auth or export ONE_AUTH=/some/other/path\r\n#api_username:\r\n#api_password:\r\nkeyed_groups:\r\n  - prefix: distro\r\n    key: ansible_distribution\r\n\r\n```\r\n\r\n`ansible-inventory --list -i opennebula.yml`\r\n\r\n### Expected Results\r\n\r\nI expected a grouping of hosts by distribution.  Note that the plugin does perform grouping by LABEL, which is seen below with the `srv` and `foo` labels/groups.\r\n\r\n### Actual Results\r\n\r\n```console (paste below)\r\n```console\r\nansible-inventory --list -i opennebula.yml\r\n{\r\n    \"_meta\": {\r\n        \"hostvars\": {\r\n            \"terraform_demo_00\": {\r\n                \"INFO\": \"\",\r\n                \"INPUTS_ORDER\": \"\",\r\n                \"LABELS\": [],\r\n                \"LOGO\": \"images/logos/logo.png\",\r\n                \"LXD_SECURITY_PRIVILEGED\": \"true\",\r\n                \"MEMORY_UNIT_COST\": \"MB\",\r\n                \"SCHED_REQUIREMENTS\": \"ARCH=\\\"x86_64\\\"\",\r\n                \"ansible_host\": \"192.168.11.235\",\r\n                \"name\": \"terraform_demo_00\",\r\n                \"v4_first_ip\": \"192.168.11.235\",\r\n                \"v6_first_ip\": false\r\n            },\r\n            \"terraform_demo_01\": {\r\n                \"INFO\": \"\",\r\n                \"INPUTS_ORDER\": \"\",\r\n                \"LABELS\": [\r\n                    \"srv\",\r\n                    \"foo\"\r\n                ],\r\n                \"LOGO\": \"images/logos/logo.png\",\r\n                \"LXD_SECURITY_PRIVILEGED\": \"true\",\r\n                \"MEMORY_UNIT_COST\": \"MB\",\r\n                \"SCHED_REQUIREMENTS\": \"ARCH=\\\"x86_64\\\"\",\r\n                \"ansible_host\": \"192.168.11.241\",\r\n                \"name\": \"terraform_demo_01\",\r\n                \"v4_first_ip\": \"192.168.11.241\",\r\n                \"v6_first_ip\": false\r\n            }\r\n        }\r\n    },\r\n    \"all\": {\r\n        \"children\": [\r\n            \"srv\",\r\n            \"foo\",\r\n            \"ungrouped\"\r\n        ]\r\n    },\r\n    \"srv\": {\r\n        \"hosts\": [\r\n            \"terraform_demo_01\"\r\n        ]\r\n    },\r\n    \"foo\": {\r\n        \"hosts\": [\r\n            \"terraform_demo_01\"\r\n        ]\r\n    },\r\n    \"ungrouped\": {\r\n        \"hosts\": [\r\n            \"terraform_demo_00\"\r\n        ]\r\n    }\r\n}\r\n```\r\n```\r\n\r\n\r\n### Code of Conduct\r\n\r\n- [X] I agree to follow the Ansible Code of Conduct\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2020, FELDSAM s.r.o. - FeldHost\u2122 <support@feldhost.cz>\n# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)\n\nfrom __future__ import (absolute_import, division, print_function)\n\n__metaclass__ = type\n\nDOCUMENTATION = r'''\n    name: opennebula\n    author:\n        - Kristian Feldsam (@feldsam)\n    short_description: OpenNebula inventory source\n    version_added: \"3.8.0\"\n    extends_documentation_fragment:\n        - constructed\n    description:\n        - Get inventory hosts from OpenNebula cloud.\n        - Uses an YAML configuration file ending with either I(opennebula.yml) or I(opennebula.yaml)\n          to set parameter values.\n        - Uses I(api_authfile), C(~/.one/one_auth), or C(ONE_AUTH) pointing to a OpenNebula credentials file.\n    options:\n        plugin:\n            description: Token that ensures this is a source file for the 'opennebula' plugin.\n            type: string\n            required: true\n            choices: [ community.general.opennebula ]\n        api_url:\n            description:\n              - URL of the OpenNebula RPC server.\n              - It is recommended to use HTTPS so that the username/password are not\n                transferred over the network unencrypted.\n              - If not set then the value of the C(ONE_URL) environment variable is used.\n            env:\n              - name: ONE_URL\n            required: True\n            type: string\n        api_username:\n            description:\n              - Name of the user to login into the OpenNebula RPC server. If not set\n                then the value of the C(ONE_USERNAME) environment variable is used.\n            env:\n              - name: ONE_USERNAME\n            type: string\n        api_password:\n            description:\n              - Password or a token of the user to login into OpenNebula RPC server.\n              - If not set, the value of the C(ONE_PASSWORD) environment variable is used.\n            env:\n              - name: ONE_PASSWORD\n            required: False\n            type: string\n        api_authfile:\n            description:\n              - If both I(api_username) or I(api_password) are not set, then it will try\n                authenticate with ONE auth file. Default path is C(~/.one/one_auth).\n              - Set environment variable C(ONE_AUTH) to override this path.\n            env:\n              - name: ONE_AUTH\n            required: False\n            type: string\n        hostname:\n            description: Field to match the hostname. Note C(v4_first_ip) corresponds to the first IPv4 found on VM.\n            type: string\n            default: v4_first_ip\n            choices:\n                - v4_first_ip\n                - v6_first_ip\n                - name\n        filter_by_label:\n            description: Only return servers filtered by this label.\n            type: string\n        group_by_labels:\n            description: Create host groups by vm labels\n            type: bool\n            default: True\n'''\n\nEXAMPLES = r'''\n# inventory_opennebula.yml file in YAML format\n# Example command line: ansible-inventory --list -i inventory_opennebula.yml\n\n# Pass a label filter to the API\nplugin: community.general.opennebula\napi_url: https://opennebula:2633/RPC2\nfilter_by_label: Cache\n'''\n\ntry:\n    import pyone\n\n    HAS_PYONE = True\nexcept ImportError:\n    HAS_PYONE = False\n\nfrom ansible.errors import AnsibleError\nfrom ansible.plugins.inventory import BaseInventoryPlugin, Constructable\nfrom ansible.module_utils.common.text.converters import to_native\n\nfrom collections import namedtuple\nimport os\n\n\nclass InventoryModule(BaseInventoryPlugin, Constructable):\n    NAME = 'community.general.opennebula'\n\n    def verify_file(self, path):\n        valid = False\n        if super(InventoryModule, self).verify_file(path):\n            if path.endswith(('opennebula.yaml', 'opennebula.yml')):\n                valid = True\n        return valid\n\n    def _get_connection_info(self):\n        url = self.get_option('api_url')\n        username = self.get_option('api_username')\n        password = self.get_option('api_password')\n        authfile = self.get_option('api_authfile')\n\n        if not username and not password:\n            if authfile is None:\n                authfile = os.path.join(os.environ.get(\"HOME\"), \".one\", \"one_auth\")\n            try:\n                with open(authfile, \"r\") as fp:\n                    authstring = fp.read().rstrip()\n                username, password = authstring.split(\":\")\n            except (OSError, IOError):\n                raise AnsibleError(\"Could not find or read ONE_AUTH file at '{e}'\".format(e=authfile))\n            except Exception:\n                raise AnsibleError(\"Error occurs when reading ONE_AUTH file at '{e}'\".format(e=authfile))\n\n        auth_params = namedtuple('auth', ('url', 'username', 'password'))\n\n        return auth_params(url=url, username=username, password=password)\n\n    def _get_vm_ipv4(self, vm):\n        nic = vm.TEMPLATE.get('NIC')\n\n        if isinstance(nic, dict):\n            nic = [nic]\n\n        for net in nic:\n            return net['IP']\n\n        return False\n\n    def _get_vm_ipv6(self, vm):\n        nic = vm.TEMPLATE.get('NIC')\n\n        if isinstance(nic, dict):\n            nic = [nic]\n\n        for net in nic:\n            if net.get('IP6_GLOBAL'):\n                return net['IP6_GLOBAL']\n\n        return False\n\n    def _get_vm_pool(self):\n        auth = self._get_connection_info()\n\n        if not (auth.username and auth.password):\n            raise AnsibleError('API Credentials missing. Check OpenNebula inventory file.')\n        else:\n            one_client = pyone.OneServer(auth.url, session=auth.username + ':' + auth.password)\n\n        # get hosts (VMs)\n        try:\n            vm_pool = one_client.vmpool.infoextended(-2, -1, -1, 3)\n        except Exception as e:\n            raise AnsibleError(\"Something happened during XML-RPC call: {e}\".format(e=to_native(e)))\n\n        return vm_pool\n\n    def _retrieve_servers(self, label_filter=None):\n        vm_pool = self._get_vm_pool()\n\n        result = []\n\n        # iterate over hosts\n        for vm in vm_pool.VM:\n            server = vm.USER_TEMPLATE\n\n            labels = []\n            if vm.USER_TEMPLATE.get('LABELS'):\n                labels = [s for s in vm.USER_TEMPLATE.get('LABELS') if s == ',' or s == '-' or s.isalnum() or s.isspace()]\n                labels = ''.join(labels)\n                labels = labels.replace(' ', '_')\n                labels = labels.replace('-', '_')\n                labels = labels.split(',')\n\n            # filter by label\n            if label_filter is not None:\n                if label_filter not in labels:\n                    continue\n\n            server['name'] = vm.NAME\n            server['LABELS'] = labels\n            server['v4_first_ip'] = self._get_vm_ipv4(vm)\n            server['v6_first_ip'] = self._get_vm_ipv6(vm)\n\n            result.append(server)\n\n        return result\n\n    def _populate(self):\n        hostname_preference = self.get_option('hostname')\n        group_by_labels = self.get_option('group_by_labels')\n\n        # Add a top group 'one'\n        self.inventory.add_group(group='all')\n\n        filter_by_label = self.get_option('filter_by_label')\n        for server in self._retrieve_servers(filter_by_label):\n            # check for labels\n            if group_by_labels and server['LABELS']:\n                for label in server['LABELS']:\n                    self.inventory.add_group(group=label)\n                    self.inventory.add_host(host=server['name'], group=label)\n\n            self.inventory.add_host(host=server['name'], group='all')\n\n            for attribute, value in server.items():\n                self.inventory.set_variable(server['name'], attribute, value)\n\n            if hostname_preference != 'name':\n                self.inventory.set_variable(server['name'], 'ansible_host', server[hostname_preference])\n\n            if server.get('SSH_PORT'):\n                self.inventory.set_variable(server['name'], 'ansible_port', server['SSH_PORT'])\n\n    def parse(self, inventory, loader, path, cache=True):\n        if not HAS_PYONE:\n            raise AnsibleError('OpenNebula Inventory plugin requires pyone to work!')\n\n        super(InventoryModule, self).parse(inventory, loader, path)\n        self._read_config_data(path=path)\n\n        self._populate()\n", "path": "plugins/inventory/opennebula.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2020, FELDSAM s.r.o. - FeldHost\u2122 <support@feldhost.cz>\n# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)\n\nfrom __future__ import (absolute_import, division, print_function)\n\n__metaclass__ = type\n\nDOCUMENTATION = r'''\n    name: opennebula\n    author:\n        - Kristian Feldsam (@feldsam)\n    short_description: OpenNebula inventory source\n    version_added: \"3.8.0\"\n    extends_documentation_fragment:\n        - constructed\n    description:\n        - Get inventory hosts from OpenNebula cloud.\n        - Uses an YAML configuration file ending with either I(opennebula.yml) or I(opennebula.yaml)\n          to set parameter values.\n        - Uses I(api_authfile), C(~/.one/one_auth), or C(ONE_AUTH) pointing to a OpenNebula credentials file.\n    options:\n        plugin:\n            description: Token that ensures this is a source file for the 'opennebula' plugin.\n            type: string\n            required: true\n            choices: [ community.general.opennebula ]\n        api_url:\n            description:\n              - URL of the OpenNebula RPC server.\n              - It is recommended to use HTTPS so that the username/password are not\n                transferred over the network unencrypted.\n              - If not set then the value of the C(ONE_URL) environment variable is used.\n            env:\n              - name: ONE_URL\n            required: True\n            type: string\n        api_username:\n            description:\n              - Name of the user to login into the OpenNebula RPC server. If not set\n                then the value of the C(ONE_USERNAME) environment variable is used.\n            env:\n              - name: ONE_USERNAME\n            type: string\n        api_password:\n            description:\n              - Password or a token of the user to login into OpenNebula RPC server.\n              - If not set, the value of the C(ONE_PASSWORD) environment variable is used.\n            env:\n              - name: ONE_PASSWORD\n            required: False\n            type: string\n        api_authfile:\n            description:\n              - If both I(api_username) or I(api_password) are not set, then it will try\n                authenticate with ONE auth file. Default path is C(~/.one/one_auth).\n              - Set environment variable C(ONE_AUTH) to override this path.\n            env:\n              - name: ONE_AUTH\n            required: False\n            type: string\n        hostname:\n            description: Field to match the hostname. Note C(v4_first_ip) corresponds to the first IPv4 found on VM.\n            type: string\n            default: v4_first_ip\n            choices:\n                - v4_first_ip\n                - v6_first_ip\n                - name\n        filter_by_label:\n            description: Only return servers filtered by this label.\n            type: string\n        group_by_labels:\n            description: Create host groups by vm labels\n            type: bool\n            default: True\n'''\n\nEXAMPLES = r'''\n# inventory_opennebula.yml file in YAML format\n# Example command line: ansible-inventory --list -i inventory_opennebula.yml\n\n# Pass a label filter to the API\nplugin: community.general.opennebula\napi_url: https://opennebula:2633/RPC2\nfilter_by_label: Cache\n'''\n\ntry:\n    import pyone\n\n    HAS_PYONE = True\nexcept ImportError:\n    HAS_PYONE = False\n\nfrom ansible.errors import AnsibleError\nfrom ansible.plugins.inventory import BaseInventoryPlugin, Constructable\nfrom ansible.module_utils.common.text.converters import to_native\n\nfrom collections import namedtuple\nimport os\n\n\nclass InventoryModule(BaseInventoryPlugin, Constructable):\n    NAME = 'community.general.opennebula'\n\n    def verify_file(self, path):\n        valid = False\n        if super(InventoryModule, self).verify_file(path):\n            if path.endswith(('opennebula.yaml', 'opennebula.yml')):\n                valid = True\n        return valid\n\n    def _get_connection_info(self):\n        url = self.get_option('api_url')\n        username = self.get_option('api_username')\n        password = self.get_option('api_password')\n        authfile = self.get_option('api_authfile')\n\n        if not username and not password:\n            if authfile is None:\n                authfile = os.path.join(os.environ.get(\"HOME\"), \".one\", \"one_auth\")\n            try:\n                with open(authfile, \"r\") as fp:\n                    authstring = fp.read().rstrip()\n                username, password = authstring.split(\":\")\n            except (OSError, IOError):\n                raise AnsibleError(\"Could not find or read ONE_AUTH file at '{e}'\".format(e=authfile))\n            except Exception:\n                raise AnsibleError(\"Error occurs when reading ONE_AUTH file at '{e}'\".format(e=authfile))\n\n        auth_params = namedtuple('auth', ('url', 'username', 'password'))\n\n        return auth_params(url=url, username=username, password=password)\n\n    def _get_vm_ipv4(self, vm):\n        nic = vm.TEMPLATE.get('NIC')\n\n        if isinstance(nic, dict):\n            nic = [nic]\n\n        for net in nic:\n            return net['IP']\n\n        return False\n\n    def _get_vm_ipv6(self, vm):\n        nic = vm.TEMPLATE.get('NIC')\n\n        if isinstance(nic, dict):\n            nic = [nic]\n\n        for net in nic:\n            if net.get('IP6_GLOBAL'):\n                return net['IP6_GLOBAL']\n\n        return False\n\n    def _get_vm_pool(self):\n        auth = self._get_connection_info()\n\n        if not (auth.username and auth.password):\n            raise AnsibleError('API Credentials missing. Check OpenNebula inventory file.')\n        else:\n            one_client = pyone.OneServer(auth.url, session=auth.username + ':' + auth.password)\n\n        # get hosts (VMs)\n        try:\n            vm_pool = one_client.vmpool.infoextended(-2, -1, -1, 3)\n        except Exception as e:\n            raise AnsibleError(\"Something happened during XML-RPC call: {e}\".format(e=to_native(e)))\n\n        return vm_pool\n\n    def _retrieve_servers(self, label_filter=None):\n        vm_pool = self._get_vm_pool()\n\n        result = []\n\n        # iterate over hosts\n        for vm in vm_pool.VM:\n            server = vm.USER_TEMPLATE\n\n            labels = []\n            if vm.USER_TEMPLATE.get('LABELS'):\n                labels = [s for s in vm.USER_TEMPLATE.get('LABELS') if s == ',' or s == '-' or s.isalnum() or s.isspace()]\n                labels = ''.join(labels)\n                labels = labels.replace(' ', '_')\n                labels = labels.replace('-', '_')\n                labels = labels.split(',')\n\n            # filter by label\n            if label_filter is not None:\n                if label_filter not in labels:\n                    continue\n\n            server['name'] = vm.NAME\n            server['LABELS'] = labels\n            server['v4_first_ip'] = self._get_vm_ipv4(vm)\n            server['v6_first_ip'] = self._get_vm_ipv6(vm)\n\n            result.append(server)\n\n        return result\n\n    def _populate(self):\n        hostname_preference = self.get_option('hostname')\n        group_by_labels = self.get_option('group_by_labels')\n        strict = self.get_option('strict')\n\n        # Add a top group 'one'\n        self.inventory.add_group(group='all')\n\n        filter_by_label = self.get_option('filter_by_label')\n        servers = self._retrieve_servers(filter_by_label)\n        for server in servers:\n            hostname = server['name']\n            # check for labels\n            if group_by_labels and server['LABELS']:\n                for label in server['LABELS']:\n                    self.inventory.add_group(group=label)\n                    self.inventory.add_host(host=hostname, group=label)\n\n            self.inventory.add_host(host=hostname, group='all')\n\n            for attribute, value in server.items():\n                self.inventory.set_variable(hostname, attribute, value)\n\n            if hostname_preference != 'name':\n                self.inventory.set_variable(hostname, 'ansible_host', server[hostname_preference])\n\n            if server.get('SSH_PORT'):\n                self.inventory.set_variable(hostname, 'ansible_port', server['SSH_PORT'])\n\n            # handle construcable implementation: get composed variables if any\n            self._set_composite_vars(self.get_option('compose'), server, hostname, strict=strict)\n\n            # groups based on jinja conditionals get added to specific groups\n            self._add_host_to_composed_groups(self.get_option('groups'), server, hostname, strict=strict)\n\n            # groups based on variables associated with them in the inventory\n            self._add_host_to_keyed_groups(self.get_option('keyed_groups'), server, hostname, strict=strict)\n\n    def parse(self, inventory, loader, path, cache=True):\n        if not HAS_PYONE:\n            raise AnsibleError('OpenNebula Inventory plugin requires pyone to work!')\n\n        super(InventoryModule, self).parse(inventory, loader, path)\n        self._read_config_data(path=path)\n\n        self._populate()\n", "path": "plugins/inventory/opennebula.py"}]}