Skip to content

Commit

Permalink
salt: Add a module function to check available Ports
Browse files Browse the repository at this point in the history
Add a module that use `metalk8s_network.get_listening_processes` to
check listening connection on the local node and check that running
process match what is expected by MetalK8s

Sees: #3092
  • Loading branch information
TeddyAndrieux committed Mar 2, 2021
1 parent d6dbc06 commit e4c8abe
Show file tree
Hide file tree
Showing 3 changed files with 493 additions and 2 deletions.
190 changes: 190 additions & 0 deletions salt/_modules/metalk8s_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import ipaddress
import re

from salt.exceptions import CheckError

Expand Down Expand Up @@ -38,6 +39,11 @@ def node(raises=True, **kwargs):
if svc_ret is not True:
errors.append(svc_ret)

# Run `ports` check
ports_ret = __salt__["metalk8s_checks.ports"](raises=False, **kwargs)
if ports_ret is not True:
errors.append(ports_ret)

# Run `route_exists` check for the Service Cluster IPs
service_cidr = kwargs.pop(
"service_cidr", __pillar__.get("networks", {}).get("service", None)
Expand Down Expand Up @@ -169,6 +175,190 @@ def services(conflicting_services=None, raises=True, **kwargs):
return error_msg or True


def ports(
listening_process=None,
raises=True,
listening_process_per_role=None,
roles=None,
**kwargs
):
"""Check if an unexpected process already listening on a port on the machine,
return a string (or raise if `raises` is set to `True`) with the list of
unexpected process and port.
Arguments:
listening_process (dict): override the list of ports expected to be
unused (or bind to a MetalK8s process).
raises (bool): the method will raise if there is any unexpected process
bind on a MetalK8s port.
listening_process_per_role (dict): If `listening_process` not provided
compute it from this dict. Dict matching between listening process
and roles (default: retrieve using map.jinja)
roles (list): List of local role for the node (default: retrieve from
pillar)
Note: `listening_process` is a dict like
```
{
'<address>': {
'expected': '<processes>',
'descritpion': '<description>',
'mandatory': 'True/False'
}
}
```
Where:
- `<address>` could be a full address `<ip>:<port>` or just a `<port>`
(if `<ip>` is equal to `control_plane_ip` or `workload_plane_ip` we replace
it with the local expected IP
- `<processes>` could be a single process regexp matching or a list of regexp
(if one of the processes in this list match then result is ok)
- `<description>` is optional, and is just a description of the expected usage
of this address
- `<mandatory>` is a boolean to force this expected process, if set to False
we expected either, the process in `expected` either nothing (default: False)
"""
if listening_process is None:
if listening_process_per_role is None:
listening_process_per_role = __salt__["metalk8s.get_from_map"](
"networks", saltenv=kwargs.get("saltenv")
)["listening_process_per_role"]
if roles is None:
roles = (
__pillar__.get("metalk8s", {})
.get("nodes", {})
.get(__grains__["id"], {})
.get("roles")
)

# If role not yet set consider we have all roles
roles = listening_process_per_role.keys()

# Compute full dict of listening process according to local `roles`
# Note: We consider all minions as "node"
listening_process = listening_process_per_role.get("node") or {}
for role in roles:
listening_process.update(listening_process_per_role.get(role) or {})

if not isinstance(listening_process, dict):
raise ValueError(
"Invalid listening process, expected dict but got {}.".format(
type(listening_process).__name__
)
)

errors = []

all_listen_connections = __salt__["metalk8s_network.get_listening_processes"]()

for address, matching in listening_process.items():
if isinstance(address, int):
address = str(address)
ip = None
port = address
if len(address.rsplit(":")) == 2:
ip, port = address.rsplit(":")

port = int(port)

if ip and ip in ["control_plane_ip", "workload_plane_ip"]:
ip = __grains__["metalk8s"][ip]

processes = matching.get("expected")
if not isinstance(processes, list):
processes = [processes]

if not matching.get("mandatory") and None not in processes:
processes.append(None)

process_on_port = all_listen_connections.get(port)

success = False
error_process = {}
# If one of this process is expected one, succeed
for process in processes:
match = True

# We expect nothing to be listening on this port
if process is None:
if process_on_port:
# Failure:
# - we expect nothing listening on this port
# - a process listen on every IPs
# - something already listening on the expected IP
# NOTE: Normaly if a process listen on `0.0.0.0` we do not
# have any other process on this port
if not ip:
error_process = process_on_port
match = False
if "0.0.0.0" in process_on_port:
error_process["0.0.0.0"] = process_on_port["0.0.0.0"]
match = False
if ip in process_on_port:
error_process[ip] = process_on_port[ip]
match = False

# We expect "<process>" to be listening on this port
# NOTE: if nothing listening it's a failure
else:
# Failure:
# - nothing listening on this ip:port
# - nothing listening on the expected IP
# - something not expected already listening
if (
not process_on_port
or ip
and "0.0.0.0" not in process_on_port
and ip not in process_on_port
):
match = False
elif "0.0.0.0" in process_on_port and not re.match(
process, process_on_port["0.0.0.0"]["name"]
):
error_process["0.0.0.0"] = process_on_port["0.0.0.0"]
match = False
elif (
ip
and ip in process_on_port
and not re.match(process, process_on_port[ip]["name"])
):
error_process[ip] = process_on_port[ip]
match = False
elif not ip:
for proc_ip, proc in process_on_port.items():
if not re.match(process, proc["name"]):
error_process[proc_ip] = proc
match = False

# This "process" match what we expect
if match:
success = True
break

if not success:
fail_msg = "{} should be listening on {} but {}.".format(
matching.get(
"description",
" or ".join(process or "nothing" for process in processes),
),
address,
"nothing listening"
if not error_process
else " and ".join(
"{}({}) listen on {}".format(proc["name"], proc["pid"], proc_ip)
for proc_ip, proc in error_process.items()
),
)

errors.append(fail_msg)

error_msg = "\n".join(errors)
if error_msg and raises:
raise CheckError(error_msg)

return error_msg or True


def sysctl(params, raises=True):
"""Check if the given sysctl key-values match the ones in memory and
return a string (or raise if `raises` is set to `True`) with the list
Expand Down
Loading

0 comments on commit e4c8abe

Please sign in to comment.