We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
nmap inventory plugin error: local variable 'results' referenced before assignment
### Summary

Using the nmap inventory plugin from collection version 3.x and 2.x I am getting the following error when attempting to do the inventory sync in Ansible:

```
[WARNING]:  * Failed to parse /tmp/awx_320_6c0cpe4z/project/hosts/nmap/nmap.yml
with ansible_collections.community.general.plugins.inventory.nmap plugin: local
variable 'results' referenced before assignment
```

I'm assuming the python code references the `results` variable before assignment. 

This error did not exist in the previous version of the collection (version 1.x).


### Issue Type

Bug Report

### Component Name

nmap inventory plugin

### Ansible Version

From Ansible Tower job output:

```console (paste below)
ansible-inventory 2.9.17
  config file = /tmp/awx_320_6c0cpe4z/project/ansible.cfg
  configured module search path = ['/var/lib/awx/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.6/site-packages/ansible
  executable location = /usr/bin/ansible-inventory
  python version = 3.6.8 (default, Aug 18 2020, 08:33:21) [GCC 8.3.1 20191121 (Red Hat 8.3.1-5)]
Using /tmp/awx_320_6c0cpe4z/project/ansible.cfg as config file
[WARNING]:  * Failed to parse /tmp/awx_320_6c0cpe4z/project/hosts/nmap/nmap.yml
with ansible_collections.community.general.plugins.inventory.nmap plugin: local
variable 'results' referenced before assignment
  File "/usr/lib/python3.6/site-packages/ansible/inventory/manager.py", line 280, in parse_source
    plugin.parse(self._inventory, self._loader, source, cache=cache)
  File "/tmp/awx_320_6c0cpe4z/requirements_collections/ansible_collections/community/general/plugins/inventory/nmap.py", line 212, in parse
    self._populate(results)
[WARNING]: Unable to parse /tmp/awx_320_6c0cpe4z/project/hosts/nmap/nmap.yml as
an inventory source
[WARNING]: Unable to parse /tmp/awx_320_6c0cpe4z/project/hosts/nmap as an
inventory source
ERROR! No inventory was parsed, please check your configuration and options.
```


### Configuration

The `nmap.yml` inventory plugin configuration.

```console (paste below)
# Inventory plugin type
plugin: community.general.nmap
# Invalid entries do not cause a fatal error and will be skipped
strict: False
# Network IP or range of IPs to scan, you can use a simple range (10.2.2.15-25) or CIDR notation.
address: 192.168.1.0/24
# list of addresses to exclude
exclude:
    - '192.168.1.0'
# Enable/disable scanning for open ports; poor performance when scanning all ports
ports: False

# At least one of ipv4 or ipv6 is required to be True, both can be True, but they cannot both be False.
# Use IPv4 type addresses
ipv4: True
# Use IPv6 type addresses
ipv6: False

# # Create vars from jinja2 expressions. (dictionary)
# compose:
#     open_ports_exist: "{{ ports | count }}"
# # Add hosts to group based on Jinja2 conditionals (dictionary)
# groups:
#     'ports_open': "{{ ports is defined }}"
# # Add hosts to group based on the values of a variable. (list)
# keyed_groups:
# - key: ports | default("none")
#   parent_group: ports
```


### OS / Environment

RHEL8 server with Ansible Tower 3.8.x and Ansible Engine 2.9.x

### Steps to Reproduce

- Create `nmap.yml` inventory plugin yaml file
- Install `nmap` locally
- Run `ansible-inventory` to test nmap inventory plugin using collection


### Expected Results

Inventory sync succeeds.

### Actual Results

Error happens (as listed above)

### 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/nmap.py`
Content:
```
1 # Copyright (c) 2017 Ansible Project
2 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3 
4 from __future__ import (absolute_import, division, print_function)
5 __metaclass__ = type
6 
7 DOCUMENTATION = '''
8     author: Unknown (!UNKNOWN)
9     name: nmap
10     short_description: Uses nmap to find hosts to target
11     description:
12         - Uses a YAML configuration file with a valid YAML extension.
13     extends_documentation_fragment:
14       - constructed
15       - inventory_cache
16     requirements:
17       - nmap CLI installed
18     options:
19         plugin:
20             description: token that ensures this is a source file for the 'nmap' plugin.
21             required: True
22             choices: ['nmap', 'community.general.nmap']
23         address:
24             description: Network IP or range of IPs to scan, you can use a simple range (10.2.2.15-25) or CIDR notation.
25             required: True
26         exclude:
27             description: list of addresses to exclude
28             type: list
29         ports:
30             description: Enable/disable scanning for open ports
31             type: boolean
32             default: True
33         ipv4:
34             description: use IPv4 type addresses
35             type: boolean
36             default: True
37         ipv6:
38             description: use IPv6 type addresses
39             type: boolean
40             default: True
41     notes:
42         - At least one of ipv4 or ipv6 is required to be True, both can be True, but they cannot both be False.
43         - 'TODO: add OS fingerprinting'
44 '''
45 EXAMPLES = '''
46 # inventory.config file in YAML format
47 plugin: community.general.nmap
48 strict: False
49 address: 192.168.0.0/24
50 '''
51 
52 import os
53 import re
54 
55 from subprocess import Popen, PIPE
56 
57 from ansible import constants as C
58 from ansible.errors import AnsibleParserError
59 from ansible.module_utils._text import to_native, to_text
60 from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
61 from ansible.module_utils.common.process import get_bin_path
62 
63 
64 class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
65 
66     NAME = 'community.general.nmap'
67     find_host = re.compile(r'^Nmap scan report for ([\w,.,-]+)(?: \(([\w,.,:,\[,\]]+)\))?')
68     find_port = re.compile(r'^(\d+)/(\w+)\s+(\w+)\s+(\w+)')
69 
70     def __init__(self):
71         self._nmap = None
72         super(InventoryModule, self).__init__()
73 
74     def _populate(self, hosts):
75         # Use constructed if applicable
76         strict = self.get_option('strict')
77 
78         for host in hosts:
79             hostname = host['name']
80             self.inventory.add_host(hostname)
81             for var, value in host.items():
82                 self.inventory.set_variable(hostname, var, value)
83 
84             # Composed variables
85             self._set_composite_vars(self.get_option('compose'), host, hostname, strict=strict)
86 
87             # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
88             self._add_host_to_composed_groups(self.get_option('groups'), host, hostname, strict=strict)
89 
90             # Create groups based on variable values and add the corresponding hosts to it
91             self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host, hostname, strict=strict)
92 
93     def verify_file(self, path):
94 
95         valid = False
96         if super(InventoryModule, self).verify_file(path):
97             file_name, ext = os.path.splitext(path)
98 
99             if not ext or ext in C.YAML_FILENAME_EXTENSIONS:
100                 valid = True
101 
102         return valid
103 
104     def parse(self, inventory, loader, path, cache=True):
105 
106         try:
107             self._nmap = get_bin_path('nmap')
108         except ValueError as e:
109             raise AnsibleParserError('nmap inventory plugin requires the nmap cli tool to work: {0}'.format(to_native(e)))
110 
111         super(InventoryModule, self).parse(inventory, loader, path, cache=cache)
112 
113         self._read_config_data(path)
114 
115         cache_key = self.get_cache_key(path)
116 
117         # cache may be True or False at this point to indicate if the inventory is being refreshed
118         # get the user's cache option too to see if we should save the cache if it is changing
119         user_cache_setting = self.get_option('cache')
120 
121         # read if the user has caching enabled and the cache isn't being refreshed
122         attempt_to_read_cache = user_cache_setting and cache
123         # update if the user has caching enabled and the cache is being refreshed; update this value to True if the cache has expired below
124         cache_needs_update = user_cache_setting and not cache
125 
126         if attempt_to_read_cache:
127             try:
128                 results = self._cache[cache_key]
129             except KeyError:
130                 # This occurs if the cache_key is not in the cache or if the cache_key expired, so the cache needs to be updated
131                 cache_needs_update = True
132 
133         if cache_needs_update:
134             # setup command
135             cmd = [self._nmap]
136             if not self._options['ports']:
137                 cmd.append('-sP')
138 
139             if self._options['ipv4'] and not self._options['ipv6']:
140                 cmd.append('-4')
141             elif self._options['ipv6'] and not self._options['ipv4']:
142                 cmd.append('-6')
143             elif not self._options['ipv6'] and not self._options['ipv4']:
144                 raise AnsibleParserError('One of ipv4 or ipv6 must be enabled for this plugin')
145 
146             if self._options['exclude']:
147                 cmd.append('--exclude')
148                 cmd.append(','.join(self._options['exclude']))
149 
150             cmd.append(self._options['address'])
151             try:
152                 # execute
153                 p = Popen(cmd, stdout=PIPE, stderr=PIPE)
154                 stdout, stderr = p.communicate()
155                 if p.returncode != 0:
156                     raise AnsibleParserError('Failed to run nmap, rc=%s: %s' % (p.returncode, to_native(stderr)))
157 
158                 # parse results
159                 host = None
160                 ip = None
161                 ports = []
162                 results = []
163 
164                 try:
165                     t_stdout = to_text(stdout, errors='surrogate_or_strict')
166                 except UnicodeError as e:
167                     raise AnsibleParserError('Invalid (non unicode) input returned: %s' % to_native(e))
168 
169                 for line in t_stdout.splitlines():
170                     hits = self.find_host.match(line)
171                     if hits:
172                         if host is not None and ports:
173                             results[-1]['ports'] = ports
174 
175                         # if dns only shows arpa, just use ip instead as hostname
176                         if hits.group(1).endswith('.in-addr.arpa'):
177                             host = hits.group(2)
178                         else:
179                             host = hits.group(1)
180 
181                         # if no reverse dns exists, just use ip instead as hostname
182                         if hits.group(2) is not None:
183                             ip = hits.group(2)
184                         else:
185                             ip = hits.group(1)
186 
187                         if host is not None:
188                             # update inventory
189                             results.append(dict())
190                             results[-1]['name'] = host
191                             results[-1]['ip'] = ip
192                             ports = []
193                         continue
194 
195                     host_ports = self.find_port.match(line)
196                     if host is not None and host_ports:
197                         ports.append({'port': host_ports.group(1),
198                                       'protocol': host_ports.group(2),
199                                       'state': host_ports.group(3),
200                                       'service': host_ports.group(4)})
201                         continue
202 
203                 # if any leftovers
204                 if host and ports:
205                     results[-1]['ports'] = ports
206 
207             except Exception as e:
208                 raise AnsibleParserError("failed to parse %s: %s " % (to_native(path), to_native(e)))
209 
210             self._cache[cache_key] = results
211 
212         self._populate(results)
213
```
--- 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/nmap.py b/plugins/inventory/nmap.py
--- a/plugins/inventory/nmap.py
+++ b/plugins/inventory/nmap.py
@@ -130,7 +130,7 @@
                 # This occurs if the cache_key is not in the cache or if the cache_key expired, so the cache needs to be updated
                 cache_needs_update = True
 
-        if cache_needs_update:
+        if not user_cache_setting or cache_needs_update:
             # setup command
             cmd = [self._nmap]
             if not self._options['ports']:
@@ -207,6 +207,7 @@
             except Exception as e:
                 raise AnsibleParserError("failed to parse %s: %s " % (to_native(path), to_native(e)))
 
+        if cache_needs_update:
             self._cache[cache_key] = results
 
         self._populate(results)

{"golden_diff": "diff --git a/plugins/inventory/nmap.py b/plugins/inventory/nmap.py\n--- a/plugins/inventory/nmap.py\n+++ b/plugins/inventory/nmap.py\n@@ -130,7 +130,7 @@\n                 # This occurs if the cache_key is not in the cache or if the cache_key expired, so the cache needs to be updated\n                 cache_needs_update = True\n \n-        if cache_needs_update:\n+        if not user_cache_setting or cache_needs_update:\n             # setup command\n             cmd = [self._nmap]\n             if not self._options['ports']:\n@@ -207,6 +207,7 @@\n             except Exception as e:\n                 raise AnsibleParserError(\"failed to parse %s: %s \" % (to_native(path), to_native(e)))\n \n+        if cache_needs_update:\n             self._cache[cache_key] = results\n \n         self._populate(results)\n", "issue": "nmap inventory plugin error: local variable 'results' referenced before assignment\n### Summary\n\nUsing the nmap inventory plugin from collection version 3.x and 2.x I am getting the following error when attempting to do the inventory sync in Ansible:\r\n\r\n```\r\n[WARNING]:  * Failed to parse /tmp/awx_320_6c0cpe4z/project/hosts/nmap/nmap.yml\r\nwith ansible_collections.community.general.plugins.inventory.nmap plugin: local\r\nvariable 'results' referenced before assignment\r\n```\r\n\r\nI'm assuming the python code references the `results` variable before assignment. \r\n\r\nThis error did not exist in the previous version of the collection (version 1.x).\r\n\n\n### Issue Type\n\nBug Report\n\n### Component Name\n\nnmap inventory plugin\n\n### Ansible Version\n\nFrom Ansible Tower job output:\r\n\r\n```console (paste below)\r\nansible-inventory 2.9.17\r\n  config file = /tmp/awx_320_6c0cpe4z/project/ansible.cfg\r\n  configured module search path = ['/var/lib/awx/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']\r\n  ansible python module location = /usr/lib/python3.6/site-packages/ansible\r\n  executable location = /usr/bin/ansible-inventory\r\n  python version = 3.6.8 (default, Aug 18 2020, 08:33:21) [GCC 8.3.1 20191121 (Red Hat 8.3.1-5)]\r\nUsing /tmp/awx_320_6c0cpe4z/project/ansible.cfg as config file\r\n[WARNING]:  * Failed to parse /tmp/awx_320_6c0cpe4z/project/hosts/nmap/nmap.yml\r\nwith ansible_collections.community.general.plugins.inventory.nmap plugin: local\r\nvariable 'results' referenced before assignment\r\n  File \"/usr/lib/python3.6/site-packages/ansible/inventory/manager.py\", line 280, in parse_source\r\n    plugin.parse(self._inventory, self._loader, source, cache=cache)\r\n  File \"/tmp/awx_320_6c0cpe4z/requirements_collections/ansible_collections/community/general/plugins/inventory/nmap.py\", line 212, in parse\r\n    self._populate(results)\r\n[WARNING]: Unable to parse /tmp/awx_320_6c0cpe4z/project/hosts/nmap/nmap.yml as\r\nan inventory source\r\n[WARNING]: Unable to parse /tmp/awx_320_6c0cpe4z/project/hosts/nmap as an\r\ninventory source\r\nERROR! No inventory was parsed, please check your configuration and options.\r\n```\r\n\n\n### Configuration\n\nThe `nmap.yml` inventory plugin configuration.\r\n\r\n```console (paste below)\r\n# Inventory plugin type\r\nplugin: community.general.nmap\r\n# Invalid entries do not cause a fatal error and will be skipped\r\nstrict: False\r\n# Network IP or range of IPs to scan, you can use a simple range (10.2.2.15-25) or CIDR notation.\r\naddress: 192.168.1.0/24\r\n# list of addresses to exclude\r\nexclude:\r\n    - '192.168.1.0'\r\n# Enable/disable scanning for open ports; poor performance when scanning all ports\r\nports: False\r\n\r\n# At least one of ipv4 or ipv6 is required to be True, both can be True, but they cannot both be False.\r\n# Use IPv4 type addresses\r\nipv4: True\r\n# Use IPv6 type addresses\r\nipv6: False\r\n\r\n# # Create vars from jinja2 expressions. (dictionary)\r\n# compose:\r\n#     open_ports_exist: \"{{ ports | count }}\"\r\n# # Add hosts to group based on Jinja2 conditionals (dictionary)\r\n# groups:\r\n#     'ports_open': \"{{ ports is defined }}\"\r\n# # Add hosts to group based on the values of a variable. (list)\r\n# keyed_groups:\r\n# - key: ports | default(\"none\")\r\n#   parent_group: ports\r\n```\r\n\n\n### OS / Environment\n\nRHEL8 server with Ansible Tower 3.8.x and Ansible Engine 2.9.x\n\n### Steps to Reproduce\n\n- Create `nmap.yml` inventory plugin yaml file\r\n- Install `nmap` locally\r\n- Run `ansible-inventory` to test nmap inventory plugin using collection\r\n\n\n### Expected Results\n\nInventory sync succeeds.\n\n### Actual Results\n\nError happens (as listed above)\n\n### Code of Conduct\n\n- [X] I agree to follow the Ansible Code of Conduct\n", "before_files": [{"content": "# Copyright (c) 2017 Ansible Project\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__metaclass__ = type\n\nDOCUMENTATION = '''\n    author: Unknown (!UNKNOWN)\n    name: nmap\n    short_description: Uses nmap to find hosts to target\n    description:\n        - Uses a YAML configuration file with a valid YAML extension.\n    extends_documentation_fragment:\n      - constructed\n      - inventory_cache\n    requirements:\n      - nmap CLI installed\n    options:\n        plugin:\n            description: token that ensures this is a source file for the 'nmap' plugin.\n            required: True\n            choices: ['nmap', 'community.general.nmap']\n        address:\n            description: Network IP or range of IPs to scan, you can use a simple range (10.2.2.15-25) or CIDR notation.\n            required: True\n        exclude:\n            description: list of addresses to exclude\n            type: list\n        ports:\n            description: Enable/disable scanning for open ports\n            type: boolean\n            default: True\n        ipv4:\n            description: use IPv4 type addresses\n            type: boolean\n            default: True\n        ipv6:\n            description: use IPv6 type addresses\n            type: boolean\n            default: True\n    notes:\n        - At least one of ipv4 or ipv6 is required to be True, both can be True, but they cannot both be False.\n        - 'TODO: add OS fingerprinting'\n'''\nEXAMPLES = '''\n# inventory.config file in YAML format\nplugin: community.general.nmap\nstrict: False\naddress: 192.168.0.0/24\n'''\n\nimport os\nimport re\n\nfrom subprocess import Popen, PIPE\n\nfrom ansible import constants as C\nfrom ansible.errors import AnsibleParserError\nfrom ansible.module_utils._text import to_native, to_text\nfrom ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable\nfrom ansible.module_utils.common.process import get_bin_path\n\n\nclass InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):\n\n    NAME = 'community.general.nmap'\n    find_host = re.compile(r'^Nmap scan report for ([\\w,.,-]+)(?: \\(([\\w,.,:,\\[,\\]]+)\\))?')\n    find_port = re.compile(r'^(\\d+)/(\\w+)\\s+(\\w+)\\s+(\\w+)')\n\n    def __init__(self):\n        self._nmap = None\n        super(InventoryModule, self).__init__()\n\n    def _populate(self, hosts):\n        # Use constructed if applicable\n        strict = self.get_option('strict')\n\n        for host in hosts:\n            hostname = host['name']\n            self.inventory.add_host(hostname)\n            for var, value in host.items():\n                self.inventory.set_variable(hostname, var, value)\n\n            # Composed variables\n            self._set_composite_vars(self.get_option('compose'), host, hostname, strict=strict)\n\n            # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group\n            self._add_host_to_composed_groups(self.get_option('groups'), host, hostname, strict=strict)\n\n            # Create groups based on variable values and add the corresponding hosts to it\n            self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host, hostname, strict=strict)\n\n    def verify_file(self, path):\n\n        valid = False\n        if super(InventoryModule, self).verify_file(path):\n            file_name, ext = os.path.splitext(path)\n\n            if not ext or ext in C.YAML_FILENAME_EXTENSIONS:\n                valid = True\n\n        return valid\n\n    def parse(self, inventory, loader, path, cache=True):\n\n        try:\n            self._nmap = get_bin_path('nmap')\n        except ValueError as e:\n            raise AnsibleParserError('nmap inventory plugin requires the nmap cli tool to work: {0}'.format(to_native(e)))\n\n        super(InventoryModule, self).parse(inventory, loader, path, cache=cache)\n\n        self._read_config_data(path)\n\n        cache_key = self.get_cache_key(path)\n\n        # cache may be True or False at this point to indicate if the inventory is being refreshed\n        # get the user's cache option too to see if we should save the cache if it is changing\n        user_cache_setting = self.get_option('cache')\n\n        # read if the user has caching enabled and the cache isn't being refreshed\n        attempt_to_read_cache = user_cache_setting and cache\n        # update if the user has caching enabled and the cache is being refreshed; update this value to True if the cache has expired below\n        cache_needs_update = user_cache_setting and not cache\n\n        if attempt_to_read_cache:\n            try:\n                results = self._cache[cache_key]\n            except KeyError:\n                # This occurs if the cache_key is not in the cache or if the cache_key expired, so the cache needs to be updated\n                cache_needs_update = True\n\n        if cache_needs_update:\n            # setup command\n            cmd = [self._nmap]\n            if not self._options['ports']:\n                cmd.append('-sP')\n\n            if self._options['ipv4'] and not self._options['ipv6']:\n                cmd.append('-4')\n            elif self._options['ipv6'] and not self._options['ipv4']:\n                cmd.append('-6')\n            elif not self._options['ipv6'] and not self._options['ipv4']:\n                raise AnsibleParserError('One of ipv4 or ipv6 must be enabled for this plugin')\n\n            if self._options['exclude']:\n                cmd.append('--exclude')\n                cmd.append(','.join(self._options['exclude']))\n\n            cmd.append(self._options['address'])\n            try:\n                # execute\n                p = Popen(cmd, stdout=PIPE, stderr=PIPE)\n                stdout, stderr = p.communicate()\n                if p.returncode != 0:\n                    raise AnsibleParserError('Failed to run nmap, rc=%s: %s' % (p.returncode, to_native(stderr)))\n\n                # parse results\n                host = None\n                ip = None\n                ports = []\n                results = []\n\n                try:\n                    t_stdout = to_text(stdout, errors='surrogate_or_strict')\n                except UnicodeError as e:\n                    raise AnsibleParserError('Invalid (non unicode) input returned: %s' % to_native(e))\n\n                for line in t_stdout.splitlines():\n                    hits = self.find_host.match(line)\n                    if hits:\n                        if host is not None and ports:\n                            results[-1]['ports'] = ports\n\n                        # if dns only shows arpa, just use ip instead as hostname\n                        if hits.group(1).endswith('.in-addr.arpa'):\n                            host = hits.group(2)\n                        else:\n                            host = hits.group(1)\n\n                        # if no reverse dns exists, just use ip instead as hostname\n                        if hits.group(2) is not None:\n                            ip = hits.group(2)\n                        else:\n                            ip = hits.group(1)\n\n                        if host is not None:\n                            # update inventory\n                            results.append(dict())\n                            results[-1]['name'] = host\n                            results[-1]['ip'] = ip\n                            ports = []\n                        continue\n\n                    host_ports = self.find_port.match(line)\n                    if host is not None and host_ports:\n                        ports.append({'port': host_ports.group(1),\n                                      'protocol': host_ports.group(2),\n                                      'state': host_ports.group(3),\n                                      'service': host_ports.group(4)})\n                        continue\n\n                # if any leftovers\n                if host and ports:\n                    results[-1]['ports'] = ports\n\n            except Exception as e:\n                raise AnsibleParserError(\"failed to parse %s: %s \" % (to_native(path), to_native(e)))\n\n            self._cache[cache_key] = results\n\n        self._populate(results)\n", "path": "plugins/inventory/nmap.py"}], "after_files": [{"content": "# Copyright (c) 2017 Ansible Project\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__metaclass__ = type\n\nDOCUMENTATION = '''\n    author: Unknown (!UNKNOWN)\n    name: nmap\n    short_description: Uses nmap to find hosts to target\n    description:\n        - Uses a YAML configuration file with a valid YAML extension.\n    extends_documentation_fragment:\n      - constructed\n      - inventory_cache\n    requirements:\n      - nmap CLI installed\n    options:\n        plugin:\n            description: token that ensures this is a source file for the 'nmap' plugin.\n            required: True\n            choices: ['nmap', 'community.general.nmap']\n        address:\n            description: Network IP or range of IPs to scan, you can use a simple range (10.2.2.15-25) or CIDR notation.\n            required: True\n        exclude:\n            description: list of addresses to exclude\n            type: list\n        ports:\n            description: Enable/disable scanning for open ports\n            type: boolean\n            default: True\n        ipv4:\n            description: use IPv4 type addresses\n            type: boolean\n            default: True\n        ipv6:\n            description: use IPv6 type addresses\n            type: boolean\n            default: True\n    notes:\n        - At least one of ipv4 or ipv6 is required to be True, both can be True, but they cannot both be False.\n        - 'TODO: add OS fingerprinting'\n'''\nEXAMPLES = '''\n# inventory.config file in YAML format\nplugin: community.general.nmap\nstrict: False\naddress: 192.168.0.0/24\n'''\n\nimport os\nimport re\n\nfrom subprocess import Popen, PIPE\n\nfrom ansible import constants as C\nfrom ansible.errors import AnsibleParserError\nfrom ansible.module_utils._text import to_native, to_text\nfrom ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable\nfrom ansible.module_utils.common.process import get_bin_path\n\n\nclass InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):\n\n    NAME = 'community.general.nmap'\n    find_host = re.compile(r'^Nmap scan report for ([\\w,.,-]+)(?: \\(([\\w,.,:,\\[,\\]]+)\\))?')\n    find_port = re.compile(r'^(\\d+)/(\\w+)\\s+(\\w+)\\s+(\\w+)')\n\n    def __init__(self):\n        self._nmap = None\n        super(InventoryModule, self).__init__()\n\n    def _populate(self, hosts):\n        # Use constructed if applicable\n        strict = self.get_option('strict')\n\n        for host in hosts:\n            hostname = host['name']\n            self.inventory.add_host(hostname)\n            for var, value in host.items():\n                self.inventory.set_variable(hostname, var, value)\n\n            # Composed variables\n            self._set_composite_vars(self.get_option('compose'), host, hostname, strict=strict)\n\n            # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group\n            self._add_host_to_composed_groups(self.get_option('groups'), host, hostname, strict=strict)\n\n            # Create groups based on variable values and add the corresponding hosts to it\n            self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host, hostname, strict=strict)\n\n    def verify_file(self, path):\n\n        valid = False\n        if super(InventoryModule, self).verify_file(path):\n            file_name, ext = os.path.splitext(path)\n\n            if not ext or ext in C.YAML_FILENAME_EXTENSIONS:\n                valid = True\n\n        return valid\n\n    def parse(self, inventory, loader, path, cache=True):\n\n        try:\n            self._nmap = get_bin_path('nmap')\n        except ValueError as e:\n            raise AnsibleParserError('nmap inventory plugin requires the nmap cli tool to work: {0}'.format(to_native(e)))\n\n        super(InventoryModule, self).parse(inventory, loader, path, cache=cache)\n\n        self._read_config_data(path)\n\n        cache_key = self.get_cache_key(path)\n\n        # cache may be True or False at this point to indicate if the inventory is being refreshed\n        # get the user's cache option too to see if we should save the cache if it is changing\n        user_cache_setting = self.get_option('cache')\n\n        # read if the user has caching enabled and the cache isn't being refreshed\n        attempt_to_read_cache = user_cache_setting and cache\n        # update if the user has caching enabled and the cache is being refreshed; update this value to True if the cache has expired below\n        cache_needs_update = user_cache_setting and not cache\n\n        if attempt_to_read_cache:\n            try:\n                results = self._cache[cache_key]\n            except KeyError:\n                # This occurs if the cache_key is not in the cache or if the cache_key expired, so the cache needs to be updated\n                cache_needs_update = True\n\n        if not user_cache_setting or cache_needs_update:\n            # setup command\n            cmd = [self._nmap]\n            if not self._options['ports']:\n                cmd.append('-sP')\n\n            if self._options['ipv4'] and not self._options['ipv6']:\n                cmd.append('-4')\n            elif self._options['ipv6'] and not self._options['ipv4']:\n                cmd.append('-6')\n            elif not self._options['ipv6'] and not self._options['ipv4']:\n                raise AnsibleParserError('One of ipv4 or ipv6 must be enabled for this plugin')\n\n            if self._options['exclude']:\n                cmd.append('--exclude')\n                cmd.append(','.join(self._options['exclude']))\n\n            cmd.append(self._options['address'])\n            try:\n                # execute\n                p = Popen(cmd, stdout=PIPE, stderr=PIPE)\n                stdout, stderr = p.communicate()\n                if p.returncode != 0:\n                    raise AnsibleParserError('Failed to run nmap, rc=%s: %s' % (p.returncode, to_native(stderr)))\n\n                # parse results\n                host = None\n                ip = None\n                ports = []\n                results = []\n\n                try:\n                    t_stdout = to_text(stdout, errors='surrogate_or_strict')\n                except UnicodeError as e:\n                    raise AnsibleParserError('Invalid (non unicode) input returned: %s' % to_native(e))\n\n                for line in t_stdout.splitlines():\n                    hits = self.find_host.match(line)\n                    if hits:\n                        if host is not None and ports:\n                            results[-1]['ports'] = ports\n\n                        # if dns only shows arpa, just use ip instead as hostname\n                        if hits.group(1).endswith('.in-addr.arpa'):\n                            host = hits.group(2)\n                        else:\n                            host = hits.group(1)\n\n                        # if no reverse dns exists, just use ip instead as hostname\n                        if hits.group(2) is not None:\n                            ip = hits.group(2)\n                        else:\n                            ip = hits.group(1)\n\n                        if host is not None:\n                            # update inventory\n                            results.append(dict())\n                            results[-1]['name'] = host\n                            results[-1]['ip'] = ip\n                            ports = []\n                        continue\n\n                    host_ports = self.find_port.match(line)\n                    if host is not None and host_ports:\n                        ports.append({'port': host_ports.group(1),\n                                      'protocol': host_ports.group(2),\n                                      'state': host_ports.group(3),\n                                      'service': host_ports.group(4)})\n                        continue\n\n                # if any leftovers\n                if host and ports:\n                    results[-1]['ports'] = ports\n\n            except Exception as e:\n                raise AnsibleParserError(\"failed to parse %s: %s \" % (to_native(path), to_native(e)))\n\n        if cache_needs_update:\n            self._cache[cache_key] = results\n\n        self._populate(results)\n", "path": "plugins/inventory/nmap.py"}]}