1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

nmap: fix cache support (#2282) (#2328)

* add cache support

* pep8 e501 fix

* revert verify_file function

* revert description update

* add changelog fragment

Co-authored-by: Dennis Israelsson <github@mdh.nu>
(cherry picked from commit 31c9ed0fe6)

Co-authored-by: Dennis Israelsson <dennis.israelsson@gmail.com>
This commit is contained in:
patchback[bot] 2021-04-22 07:47:45 +02:00 committed by GitHub
parent 457c92c8e2
commit c7736ab921
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 108 additions and 61 deletions

View file

@ -0,0 +1,2 @@
bugfixes:
- nmap inventory plugin - fix cache and constructed group support (https://github.com/ansible-collections/community.general/issues/2242).

View file

@ -72,6 +72,25 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
self._nmap = None self._nmap = None
super(InventoryModule, self).__init__() super(InventoryModule, self).__init__()
def _populate(self, hosts):
# Use constructed if applicable
strict = self.get_option('strict')
for host in hosts:
hostname = host['name']
self.inventory.add_host(hostname)
for var, value in host.items():
self.inventory.set_variable(hostname, var, value)
# Composed variables
self._set_composite_vars(self.get_option('compose'), host, hostname, strict=strict)
# Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
self._add_host_to_composed_groups(self.get_option('groups'), host, hostname, strict=strict)
# Create groups based on variable values and add the corresponding hosts to it
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host, hostname, strict=strict)
def verify_file(self, path): def verify_file(self, path):
valid = False valid = False
@ -83,7 +102,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
return valid return valid
def parse(self, inventory, loader, path, cache=False): def parse(self, inventory, loader, path, cache=True):
try: try:
self._nmap = get_bin_path('nmap') self._nmap = get_bin_path('nmap')
@ -94,75 +113,101 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
self._read_config_data(path) self._read_config_data(path)
# setup command cache_key = self.get_cache_key(path)
cmd = [self._nmap]
if not self._options['ports']:
cmd.append('-sP')
if self._options['ipv4'] and not self._options['ipv6']: # cache may be True or False at this point to indicate if the inventory is being refreshed
cmd.append('-4') # get the user's cache option too to see if we should save the cache if it is changing
elif self._options['ipv6'] and not self._options['ipv4']: user_cache_setting = self.get_option('cache')
cmd.append('-6')
elif not self._options['ipv6'] and not self._options['ipv4']:
raise AnsibleParserError('One of ipv4 or ipv6 must be enabled for this plugin')
if self._options['exclude']: # read if the user has caching enabled and the cache isn't being refreshed
cmd.append('--exclude') attempt_to_read_cache = user_cache_setting and cache
cmd.append(','.join(self._options['exclude'])) # update if the user has caching enabled and the cache is being refreshed; update this value to True if the cache has expired below
cache_needs_update = user_cache_setting and not cache
cmd.append(self._options['address'])
try:
# execute
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
raise AnsibleParserError('Failed to run nmap, rc=%s: %s' % (p.returncode, to_native(stderr)))
# parse results
host = None
ip = None
ports = []
if attempt_to_read_cache:
try: try:
t_stdout = to_text(stdout, errors='surrogate_or_strict') results = self._cache[cache_key]
except UnicodeError as e: except KeyError:
raise AnsibleParserError('Invalid (non unicode) input returned: %s' % to_native(e)) # 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
for line in t_stdout.splitlines(): if cache_needs_update:
hits = self.find_host.match(line) # setup command
if hits: cmd = [self._nmap]
if host is not None: if not self._options['ports']:
self.inventory.set_variable(host, 'ports', ports) cmd.append('-sP')
# if dns only shows arpa, just use ip instead as hostname if self._options['ipv4'] and not self._options['ipv6']:
if hits.group(1).endswith('.in-addr.arpa'): cmd.append('-4')
host = hits.group(2) elif self._options['ipv6'] and not self._options['ipv4']:
else: cmd.append('-6')
host = hits.group(1) elif not self._options['ipv6'] and not self._options['ipv4']:
raise AnsibleParserError('One of ipv4 or ipv6 must be enabled for this plugin')
# if no reverse dns exists, just use ip instead as hostname if self._options['exclude']:
if hits.group(2) is not None: cmd.append('--exclude')
ip = hits.group(2) cmd.append(','.join(self._options['exclude']))
else:
ip = hits.group(1)
if host is not None: cmd.append(self._options['address'])
# update inventory try:
self.inventory.add_host(host) # execute
self.inventory.set_variable(host, 'ip', ip) p = Popen(cmd, stdout=PIPE, stderr=PIPE)
ports = [] stdout, stderr = p.communicate()
continue if p.returncode != 0:
raise AnsibleParserError('Failed to run nmap, rc=%s: %s' % (p.returncode, to_native(stderr)))
host_ports = self.find_port.match(line) # parse results
if host is not None and host_ports: host = None
ports.append({'port': host_ports.group(1), 'protocol': host_ports.group(2), 'state': host_ports.group(3), 'service': host_ports.group(4)}) ip = None
continue ports = []
results = []
# TODO: parse more data, OS? try:
t_stdout = to_text(stdout, errors='surrogate_or_strict')
except UnicodeError as e:
raise AnsibleParserError('Invalid (non unicode) input returned: %s' % to_native(e))
# if any leftovers for line in t_stdout.splitlines():
if host and ports: hits = self.find_host.match(line)
self.inventory.set_variable(host, 'ports', ports) if hits:
if host is not None and ports:
results[-1]['ports'] = ports
except Exception as e: # if dns only shows arpa, just use ip instead as hostname
raise AnsibleParserError("failed to parse %s: %s " % (to_native(path), to_native(e))) if hits.group(1).endswith('.in-addr.arpa'):
host = hits.group(2)
else:
host = hits.group(1)
# if no reverse dns exists, just use ip instead as hostname
if hits.group(2) is not None:
ip = hits.group(2)
else:
ip = hits.group(1)
if host is not None:
# update inventory
results.append(dict())
results[-1]['name'] = host
results[-1]['ip'] = ip
ports = []
continue
host_ports = self.find_port.match(line)
if host is not None and host_ports:
ports.append({'port': host_ports.group(1),
'protocol': host_ports.group(2),
'state': host_ports.group(3),
'service': host_ports.group(4)})
continue
# if any leftovers
if host and ports:
results[-1]['ports'] = ports
except Exception as e:
raise AnsibleParserError("failed to parse %s: %s " % (to_native(path), to_native(e)))
self._cache[cache_key] = results
self._populate(results)