From 245cee0ece10a3a4259dd9bc6973ebd1e94df7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ga=C3=9Fner?= <40096303+moonrail@users.noreply.github.com> Date: Tue, 16 Nov 2021 19:50:29 +0100 Subject: [PATCH] listen_ports_facts: Added support for ss (#3708) --- ...3708-listen_ports_facts-add-ss-support.yml | 3 + plugins/modules/system/listen_ports_facts.py | 127 ++++++++++++++++-- .../targets/listen_ports_facts/tasks/main.yml | 37 ++--- 3 files changed, 141 insertions(+), 26 deletions(-) create mode 100644 changelogs/fragments/3708-listen_ports_facts-add-ss-support.yml diff --git a/changelogs/fragments/3708-listen_ports_facts-add-ss-support.yml b/changelogs/fragments/3708-listen_ports_facts-add-ss-support.yml new file mode 100644 index 0000000000..57909a3ef7 --- /dev/null +++ b/changelogs/fragments/3708-listen_ports_facts-add-ss-support.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - listen_ports_facts - add support for ``ss`` command besides ``netstat`` (https://github.com/ansible-collections/community.general/pull/3708). diff --git a/plugins/modules/system/listen_ports_facts.py b/plugins/modules/system/listen_ports_facts.py index c81977d7f4..1c12989d20 100644 --- a/plugins/modules/system/listen_ports_facts.py +++ b/plugins/modules/system/listen_ports_facts.py @@ -13,11 +13,25 @@ module: listen_ports_facts author: - Nathan Davison (@ndavison) description: - - Gather facts on processes listening on TCP and UDP ports using netstat command. + - Gather facts on processes listening on TCP and UDP ports using the C(netstat) or C(ss) commands. - This module currently supports Linux only. requirements: - - netstat + - netstat or ss short_description: Gather facts on processes listening on TCP and UDP ports. +notes: + - | + C(ss) returns all processes for each listen address and port. + This plugin will return each of them, so multiple entries for the same listen address and port are likely in results. +options: + command: + description: + - Override which command to use for fetching listen ports. + - 'By default module will use first found supported command on the system (in alphanumerical order).' + type: str + choices: + - netstat + - ss + version_added: 4.1.0 ''' EXAMPLES = r''' @@ -181,10 +195,87 @@ def netStatParse(raw): return results -def main(): +def ss_parse(raw): + results = list() + regex_conns = re.compile(pattern=r'\[?(.+?)\]?:([0-9]+)') + regex_pid = re.compile(pattern=r'"(.*?)",pid=(\d+)') + lines = raw.splitlines() + + if len(lines) == 0 or not lines[0].startswith('Netid '): + # unexpected stdout from ss + raise EnvironmentError('Unknown stdout format of `ss`: {0}'.format(raw)) + + # skip headers (-H arg is not present on e.g. Ubuntu 16) + lines = lines[1:] + + for line in lines: + cells = line.split(None, 6) + try: + if len(cells) == 6: + # no process column, e.g. due to unprivileged user + process = str() + protocol, state, recv_q, send_q, local_addr_port, peer_addr_port = cells + else: + protocol, state, recv_q, send_q, local_addr_port, peer_addr_port, process = cells + except ValueError: + # unexpected stdout from ss + raise EnvironmentError( + 'Expected `ss` table layout "Netid, State, Recv-Q, Send-Q, Local Address:Port, Peer Address:Port" and optionally "Process", \ + but got something else: {0}'.format(line) + ) + + conns = regex_conns.search(local_addr_port) + pids = regex_pid.findall(process) + if conns is None and pids is None: + continue + + if pids is None: + # likely unprivileged user, so add empty name & pid + # as we do in netstat logic to be consistent with output + pids = [(str(), 0)] + + address = conns.group(1) + port = conns.group(2) + for name, pid in pids: + result = { + 'pid': int(pid), + 'address': address, + 'port': int(port), + 'protocol': protocol, + 'name': name + } + results.append(result) + return results + + +def main(): + commands_map = { + 'netstat': { + 'args': [ + '-p', + '-l', + '-u', + '-n', + '-t', + ], + 'parse_func': netStatParse + }, + 'ss': { + 'args': [ + '-p', + '-l', + '-u', + '-n', + '-t', + ], + 'parse_func': ss_parse + }, + } module = AnsibleModule( - argument_spec={}, + argument_spec=dict( + command=dict(type='str', choices=list(sorted(commands_map))) + ), supports_check_mode=True, ) @@ -220,18 +311,34 @@ def main(): } try: - netstat_cmd = module.get_bin_path('netstat', True) + command = None + bin_path = None + if module.params['command'] is not None: + command = module.params['command'] + bin_path = module.get_bin_path(command, required=True) + else: + for c in sorted(commands_map): + bin_path = module.get_bin_path(c, required=False) + if bin_path is not None: + command = c + break + + if bin_path is None: + raise EnvironmentError(msg='Unable to find any of the supported commands in PATH: {0}'.format(", ".join(sorted(commands_map)))) # which ports are listening for connections? - rc, stdout, stderr = module.run_command([netstat_cmd, '-plunt']) + args = commands_map[command]['args'] + rc, stdout, stderr = module.run_command([bin_path] + args) if rc == 0: - netstatOut = netStatParse(stdout) - for p in netstatOut: + parse_func = commands_map[command]['parse_func'] + results = parse_func(stdout) + + for p in results: p['stime'] = getPidSTime(p['pid']) p['user'] = getPidUser(p['pid']) - if p['protocol'] == 'tcp': + if p['protocol'].startswith('tcp'): result['ansible_facts']['tcp_listen'].append(p) - elif p['protocol'] == 'udp': + elif p['protocol'].startswith('udp'): result['ansible_facts']['udp_listen'].append(p) except (KeyError, EnvironmentError) as e: module.fail_json(msg=to_native(e)) diff --git a/tests/integration/targets/listen_ports_facts/tasks/main.yml b/tests/integration/targets/listen_ports_facts/tasks/main.yml index 906e82c651..a6915504c9 100644 --- a/tests/integration/targets/listen_ports_facts/tasks/main.yml +++ b/tests/integration/targets/listen_ports_facts/tasks/main.yml @@ -9,30 +9,25 @@ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - name: install netstat and netcat on deb - apt: - name: "{{ item }}" + ansible.builtin.package: + name: + - net-tools + - netcat state: latest - with_items: - - net-tools - - netcat when: ansible_os_family == "Debian" - name: install netstat and netcat on rh < 7 - yum: - name: "{{ item }}" + ansible.builtin.package: + name: + - net-tools + - nc.x86_64 state: latest - with_items: - - net-tools - - nc.x86_64 when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 7 -- name: install netstat and netcat on rh >= 7 - yum: - name: "{{ item }}" +- name: install netcat on rh >= 7 + ansible.builtin.package: + name: 'nmap-ncat' state: latest - with_items: - - net-tools - - nmap-ncat when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 - name: start UDP server on port 5555 @@ -63,6 +58,16 @@ listen_ports_facts: when: ansible_os_family == "RedHat" or ansible_os_family == "Debian" +- name: Gather listening ports facts explicitly via netstat + listen_ports_facts: + command: 'netstat' + when: (ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 7) or ansible_os_family == "Debian" + +- name: Gather listening ports facts explicitly via ss + listen_ports_facts: + command: 'ss' + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 + - name: check for ansible_facts.udp_listen exists assert: that: ansible_facts.udp_listen is defined