Merge "Implement L3 firewall basic infrastructure for platform networks"

This commit is contained in:
Zuul 2023-05-11 18:52:31 +00:00 committed by Gerrit Code Review
commit c423cd939c
6 changed files with 1975 additions and 1 deletions

View File

@ -70,6 +70,7 @@ systemconfig.puppet_plugins =
040_rook = sysinv.puppet.rook:RookPuppet
041_certalarm = sysinv.puppet.certalarm:CertAlarmPuppet
042_sssd = sysinv.puppet.sssd:SssdPuppet
043_platform_firewall = sysinv.puppet.platform_firewall:PlatformFirewallPuppet
099_service_parameter = sysinv.puppet.service_parameter:ServiceParamPuppet
systemconfig.fluxcd.kustomize_ops =

View File

@ -0,0 +1,64 @@
#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# the ports below are configured via service-parameter, they cannot be statically set
# 8080: "horizon https",
# 8443: "horizon https",
# list of ports to be open in the system controller side
SYSTEMCONTROLLER = \
{"tcp":
{
22: "ssh",
389: "openLDAP",
636: "openLDAP",
4546: "stx-nfv",
5001: "keystone-api",
5492: "patching-api",
6386: "sysinv-api",
6443: "K8s API server",
8220: "dcdbsync-api",
9001: "Docker registry",
9002: "Registry token server",
9312: "barbican-api",
18003: "stx-fault",
31001: "Elastic Dashboard and API",
31090: "Kafka Brokers (NodePort)",
31091: "Kafka Brokers (NodePort)",
31092: "Kafka Brokers (NodePort)",
31093: "Kafka Brokers (NodePort)",
31094: "Kafka Brokers (NodePort)",
31095: "Kafka Brokers (NodePort)",
31096: "Kafka Brokers (NodePort)",
31097: "Kafka Brokers (NodePort)",
31098: "Kafka Brokers (NodePort)",
31099: "Kafka Brokers (NodePort)"
},
"udp":
{
162: "snmp trap"
}}
# list of ports to be open in the subcloud side
SUBCLOUD = \
{"tcp":
{
22: "ssh",
4546: "stx-nfv",
5001: "keystone-api",
5492: "patching-api",
6386: "sysinv-api",
8220: "dcdbsync-api",
9001: "Docker registry",
9002: "Registry token server",
9312: "barbican-api",
18003: "stx-fault",
31001: "Elastic Dashboard and API"
},
"udp":
{
162: "snmp trap"
}}

View File

@ -5033,3 +5033,17 @@ class Connection(object):
:param values: a dictionary with the respective fields and values to be updated in the db entry.
"""
@abc.abstractmethod
def address_pool_get(self, address_pool_uuid):
""" Get address-pool object
:param address_pool_uuid: address pool unique identifier
"""
@abc.abstractmethod
def network_get(self, network_uuid):
""" get network object
:param network_uuid: network unique identifier
"""

View File

@ -0,0 +1,431 @@
# Copyright (c) 2017-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import copy
from netaddr import IPAddress
from oslo_log import log
from sysinv.common import constants
from sysinv.common import platform_firewall as firewall
from sysinv.puppet import base
from sysinv.puppet import interface as puppet_intf
LOG = log.getLogger(__name__)
FIREWALL_GNP_MGMT_CFG = 'platform::firewall::calico::mgmt::config'
FIREWALL_GNP_CLUSTER_HOST_CFG = 'platform::firewall::calico::cluster_host::config'
FIREWALL_GNP_PXEBOOT_CFG = 'platform::firewall::calico::pxeboot::config'
FIREWALL_GNP_STORAGE_CFG = 'platform::firewall::calico::storage::config'
FIREWALL_GNP_ADMIN_CFG = 'platform::firewall::calico::admin::config'
FIREWALL_HE_INTERFACE_CFG = 'platform::firewall::calico::hostendpoint::config'
PLATFORM_FIREWALL_CLASSES = {constants.NETWORK_TYPE_PXEBOOT: FIREWALL_GNP_PXEBOOT_CFG,
constants.NETWORK_TYPE_MGMT: FIREWALL_GNP_MGMT_CFG,
constants.NETWORK_TYPE_CLUSTER_HOST: FIREWALL_GNP_CLUSTER_HOST_CFG,
constants.NETWORK_TYPE_STORAGE: FIREWALL_GNP_STORAGE_CFG,
constants.NETWORK_TYPE_ADMIN: FIREWALL_GNP_ADMIN_CFG}
class PlatformFirewallPuppet(base.BasePuppet):
""" This class handles the platform firewall hiera data generation for puppet
"""
def __init__(self, *args, **kwargs):
super(PlatformFirewallPuppet, self).__init__(*args, **kwargs)
def get_host_config(self, host):
""" Plugin public method
:param host: a sysinv.object.host class object
return: a dict containing the hiera data information
"""
config = {
FIREWALL_HE_INTERFACE_CFG: {},
FIREWALL_GNP_MGMT_CFG: {},
FIREWALL_GNP_PXEBOOT_CFG: {},
FIREWALL_GNP_CLUSTER_HOST_CFG: {},
FIREWALL_GNP_STORAGE_CFG: {},
FIREWALL_GNP_ADMIN_CFG: {}
}
dc_role = _get_dc_role(self.dbapi)
if (dc_role is not None and _exclude_DC_system()):
LOG.info("Do not apply platform firewall to DC installations for now")
return config
if (host.personality == constants.STORAGE):
LOG.info("Do not add calico firewall to storage nodes (they do not run k8s)")
return config
firewall_networks = set()
intf_ep = dict()
for ifname in self.context['interfaces'].keys():
intf = self.context['interfaces'][ifname]
if (intf.ifclass == constants.INTERFACE_CLASS_PLATFORM
and intf.iftype != constants.INTERFACE_TYPE_VIRTUAL
and intf.iftype != constants.INTERFACE_TYPE_VF):
intf_networks = self.dbapi.interface_network_get_by_interface(intf.id)
iftype_lbl = list()
for intf_network in intf_networks:
network = self.dbapi.network_get(intf_network.network_uuid)
if (network.type == constants.NETWORK_TYPE_OAM):
continue
if network.pool_uuid:
iftype_lbl.append(network.type)
firewall_networks.add(network)
intf_ep[intf.uuid] = [intf, ""]
iftype_lbl.sort()
if (intf.uuid in intf_ep.keys()):
intf_ep[intf.uuid][1] = '.'.join(iftype_lbl)
if (firewall_networks):
self._get_hostendpoints(host, intf_ep, config[FIREWALL_HE_INTERFACE_CFG])
self._get_basic_firewall_gnp(host, firewall_networks, config)
networks = {network.type: network for network in firewall_networks}
if _activate_filtering():
if (config[FIREWALL_GNP_MGMT_CFG]):
self._set_rules_mgmt(config[FIREWALL_GNP_MGMT_CFG],
networks[constants.NETWORK_TYPE_MGMT])
if (config[FIREWALL_GNP_CLUSTER_HOST_CFG]):
self._set_rules_cluster_host(config[FIREWALL_GNP_CLUSTER_HOST_CFG],
networks[constants.NETWORK_TYPE_CLUSTER_HOST])
if (config[FIREWALL_GNP_PXEBOOT_CFG]):
self._set_rules_pxeboot(config[FIREWALL_GNP_PXEBOOT_CFG],
networks[constants.NETWORK_TYPE_PXEBOOT])
if (config[FIREWALL_GNP_STORAGE_CFG]):
self._set_rules_storage(config[FIREWALL_GNP_STORAGE_CFG],
networks[constants.NETWORK_TYPE_STORAGE])
if (host.personality == constants.CONTROLLER):
if (dc_role == constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD):
if (config[FIREWALL_GNP_ADMIN_CFG]):
self._set_rules_subcloud_admin(config[FIREWALL_GNP_ADMIN_CFG],
networks[constants.NETWORK_TYPE_ADMIN],
host.personality)
else:
self._set_rules_subcloud_mgmt(config[FIREWALL_GNP_MGMT_CFG],
networks[constants.NETWORK_TYPE_MGMT],
host.personality)
if (dc_role == constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER):
self._set_rules_systemcontroller(config[FIREWALL_GNP_MGMT_CFG],
networks[constants.NETWORK_TYPE_MGMT],
host.personality)
return config
def _get_hostendpoints(self, host, intf_ep, config):
""" Fill the HostEndpoint hiera data
:param host: a sysinv.object.host class object
:param intf_ep: a dict with key as interface uuid and value with a list with the
sysinv.object.interface object and the string to be used in the ifname
label
:param config: the dict containing the hiera data to be filled
"""
for uuid in intf_ep.keys():
intf = intf_ep[uuid][0]
iftype = intf_ep[uuid][1]
host_endpoints = dict()
hep_name = host.hostname + "-" + intf.ifname + "-if-hep"
host_endpoints["apiVersion"] = "crd.projectcalico.org/v1"
host_endpoints["kind"] = "HostEndpoint"
host_endpoints.update({"metadata": dict()})
host_endpoints["metadata"].update({"name": hep_name})
host_endpoints["metadata"].update({"labels": dict()})
host_endpoints["metadata"]["labels"].update({"nodetype": host.personality})
host_endpoints["metadata"]["labels"].update({"ifname":
f"{host.hostname}.{intf.ifname}"})
host_endpoints["metadata"]["labels"].update({"iftype": iftype})
host_endpoints.update({"spec": dict()})
host_endpoints["spec"].update({"node": host.hostname})
# host_endpoints["spec"].update({"expectedIPs": list()})
interfaceName = puppet_intf.get_interface_os_ifname(self.context, intf)
host_endpoints["spec"].update({"interfaceName": interfaceName})
config[hep_name] = copy.copy(host_endpoints)
def _get_basic_firewall_gnp(self, host, firewall_networks, config):
""" Fill the GlobalNetworkPolicy basic hiera data (no filter rules)
:param host: a sysinv.object.host class object
:param firewall_networks: a set containing the platform networks that will require a
firewall to be configured.
:param config: the dict containing the hiera data to be filled
"""
for network in firewall_networks:
if (network.type == constants.NETWORK_TYPE_OAM):
continue
gnp_name = host.personality + "-" + network.type + "-if-gnp"
addr_pool = self.dbapi.address_pool_get(network.pool_uuid)
ip_version = IPAddress(f"{addr_pool.network}").version
nodetype_selector = f"has(nodetype) && nodetype == '{host.personality}'"
iftype_selector = f"has(iftype) && iftype contains '{network.type}'"
selector = f"{nodetype_selector} && {iftype_selector}"
firewall_gnp = dict()
firewall_gnp["apiVersion"] = "crd.projectcalico.org/v1"
firewall_gnp["kind"] = "GlobalNetworkPolicy"
firewall_gnp["metadata"] = {"name": gnp_name}
firewall_gnp["spec"] = dict()
firewall_gnp["spec"].update({"applyOnForward": True})
firewall_gnp["spec"].update({"order": 100})
firewall_gnp["spec"].update({"selector": selector})
firewall_gnp["spec"].update({"types": ["Ingress", "Egress"]})
firewall_gnp["spec"].update({"egress": list()})
for proto in ["TCP", "UDP", "ICMP"]:
rule = {"metadata": dict()}
rule["metadata"] = {"annotations": dict()}
rule["metadata"]["annotations"] = {"name":
f"stx-egr-{host.personality}-{network.type}-{proto.lower()}{ip_version}"}
rule.update({"protocol": proto})
rule.update({"ipVersion": ip_version})
rule.update({"action": "Allow"})
firewall_gnp["spec"]["egress"].append(rule)
firewall_gnp["spec"].update({"ingress": list()})
for proto in ["TCP", "UDP", "ICMP"]:
rule = {"metadata": dict()}
rule["metadata"] = {"annotations": dict()}
rule["metadata"]["annotations"] = {"name":
f"stx-ingr-{host.personality}-{network.type}-{proto.lower()}{ip_version}"}
rule.update({"protocol": proto})
rule.update({"ipVersion": ip_version})
rule.update({"action": "Allow"})
firewall_gnp["spec"]["ingress"].append(rule)
config[PLATFORM_FIREWALL_CLASSES[network.type]] = copy.copy(firewall_gnp)
def _set_rules_mgmt(self, gnp_config, network):
""" Fill the management network specific filtering data
:param gnp_config: the dict containing the hiera data to be filled
:param network: the sysinv.object.network object for this network
"""
addr_pool = self.dbapi.address_pool_get(network.pool_uuid)
self._add_source_net_filter(gnp_config["spec"]["ingress"],
f"{addr_pool.network}/{addr_pool.prefix}")
def _set_rules_cluster_host(self, gnp_config, network):
""" Fill the cluster-host network specific filtering data
:param gnp_config: the dict containing the hiera data to be filled
:param network: the sysinv.object.network object for this network
"""
addr_pool = self.dbapi.address_pool_get(network.pool_uuid)
self._add_source_net_filter(gnp_config["spec"]["ingress"],
f"{addr_pool.network}/{addr_pool.prefix}")
def _set_rules_pxeboot(self, gnp_config, network):
""" Fill the pxeboot network specific filtering data
:param gnp_config: the dict containing the hiera data to be filled
:param network: the sysinv.object.network object for this network
"""
addr_pool = self.dbapi.address_pool_get(network.pool_uuid)
self._add_source_net_filter(gnp_config["spec"]["ingress"],
f"{addr_pool.network}/{addr_pool.prefix}")
def _set_rules_storage(self, gnp_config, network):
""" Fill the storage network specific filtering data
:param gnp_config: the dict containing the hiera data to be filled
:param network: the sysinv.object.network object for this network
"""
addr_pool = self.dbapi.address_pool_get(network.pool_uuid)
self._add_source_net_filter(gnp_config["spec"]["ingress"],
f"{addr_pool.network}/{addr_pool.prefix}")
def _add_source_net_filter(self, rule_list, source_net):
""" Add source network in the rule list
:param rule_list: the list containing the firewall rules that need to receive the source
network value
:param source_net: the string containing the value
"""
for rule in rule_list:
if ("source" in rule.keys()):
if ("nets" in rule["source"].keys()):
rule["source"]["nets"].append(source_net)
else:
rule["source"].update({"nets": [source_net]})
else:
rule.update({"source": {"nets": [source_net]}})
def _set_rules_subcloud_admin(self, gnp_config, network, host_personality):
""" Add filtering rules for admin network in a subcloud installation
:param gnp_config: the dict containing the hiera data to be filled
:param network: the sysinv.object.network object for this network
:param host_personality: the node personality (controller, storage, or worker)
"""
# the admin network is a special case that is not needed for internal cluster communication,
# only for communication with the System Controller
gnp_config["spec"]["ingress"].clear()
addr_pool = self.dbapi.address_pool_get(network.pool_uuid)
ip_version = IPAddress(f"{addr_pool.network}").version
for proto in ["TCP", "UDP", "ICMP"]:
rule = {"metadata": dict()}
rule["metadata"] = {"annotations": dict()}
rule["metadata"]["annotations"] = {"name":
f"stx-ingr-{host_personality}-subcloud-{proto.lower()}{ip_version}"}
rule.update({"protocol": proto})
rule.update({"ipVersion": ip_version})
rule.update({"action": "Allow"})
if (proto == "TCP"):
rule.update({"destination": {"ports": self._get_subcloud_tcp_ports()}})
elif (proto == "UDP"):
rule.update({"destination": {"ports": self._get_subcloud_udp_ports()}})
gnp_config["spec"]["ingress"].append(rule)
def _set_rules_subcloud_mgmt(self, gnp_config, network, host_personality):
""" Add filtering rules for mgmt network in a subcloud installation
If the subcloud keeps using the mgmt network to communicate with the system controller
we add the L4 port filtering into this GNP
:param gnp_config: the dict containing the hiera data to be filled
:param network: the sysinv.object.network object for this network
:param host_personality: the node personality (controller, storage, or worker)
"""
addr_pool = self.dbapi.address_pool_get(network.pool_uuid)
ip_version = IPAddress(f"{addr_pool.network}").version
for proto in ["TCP", "UDP", "ICMP"]:
rule = {"metadata": dict()}
rule["metadata"] = {"annotations": dict()}
rule["metadata"]["annotations"] = {"name":
f"stx-ingr-{host_personality}-subcloud-{proto.lower()}{ip_version}"}
rule.update({"protocol": proto})
rule.update({"ipVersion": ip_version})
rule.update({"action": "Allow"})
if (proto == "TCP"):
rule.update({"destination": {"ports": self._get_subcloud_tcp_ports()}})
elif (proto == "UDP"):
rule.update({"destination": {"ports": self._get_subcloud_udp_ports()}})
gnp_config["spec"]["ingress"].append(rule)
def _set_rules_systemcontroller(self, gnp_config, network, host_personality):
""" Add filtering rules for mgmt network in a system controller installation
:param gnp_config: the dict containing the hiera data to be filled
:param network: the sysinv.object.network object for this network
:param host_personality: the node personality (controller, storage, or worker)
"""
addr_pool = self.dbapi.address_pool_get(network.pool_uuid)
ip_version = IPAddress(f"{addr_pool.network}").version
for proto in ["TCP", "UDP", "ICMP"]:
rule = {"metadata": dict()}
rule["metadata"] = {"annotations": dict()}
rule["metadata"]["annotations"] = {"name":
f"stx-ingr-{host_personality}-systemcontroller-{proto.lower()}{ip_version}"}
rule.update({"protocol": proto})
rule.update({"ipVersion": ip_version})
rule.update({"action": "Allow"})
if (proto == "TCP"):
tcp_list = self._get_systemcontroller_tcp_ports()
rule.update({"destination": {"ports": tcp_list}})
elif (proto == "UDP"):
udp_list = self._get_systemcontroller_udp_ports()
rule.update({"destination": {"ports": udp_list}})
gnp_config["spec"]["ingress"].append(rule)
def _get_subcloud_tcp_ports(self):
""" Get the TCP L4 ports for subclouds
"""
port_list = list(firewall.SUBCLOUD["tcp"].keys())
port_list.append(self._get_http_service_port())
port_list.sort()
return port_list
def _get_subcloud_udp_ports(self):
""" Get the UDP L4 ports for subclouds
"""
port_list = list(firewall.SUBCLOUD["udp"].keys())
port_list.sort()
return port_list
def _get_systemcontroller_tcp_ports(self):
""" Get the TCP L4 ports for systemcontroller
"""
port_list = list(firewall.SYSTEMCONTROLLER["tcp"].keys())
port_list.append(self._get_http_service_port())
port_list.sort()
return port_list
def _get_systemcontroller_udp_ports(self):
""" Get the UDP L4 ports for systemcontroller
"""
port_list = list(firewall.SYSTEMCONTROLLER["udp"].keys())
port_list.sort()
return port_list
def _get_http_service_port(self):
""" Get the HTTP port from the service-parameter database
"""
tcp_port = 0
if _is_https_enabled(self.dbapi):
https_port = self.dbapi.service_parameter_get_one(service="http",
section="config",
name="https_port")
tcp_port = int(https_port.value)
else:
http_port = self.dbapi.service_parameter_get_one(service="http",
section="config",
name="http_port")
tcp_port = int(http_port.value)
return tcp_port
def _activate_filtering():
""" Temporary function to be removed at the end of the feature implementation
"""
return False
def _exclude_DC_system():
""" Temporary function to be removed at the end of the feature implementation
"""
return True
def _get_dc_role(dbapi):
""" Get the DC role from the i_system database
"""
if dbapi is None:
return None
system = dbapi.isystem_get_one()
system_dc_role = system.get('distributed_cloud_role', None)
return system_dc_role
def _is_https_enabled(dbapi):
""" Get the HTTPS enabled from the i_system database
"""
if dbapi is None:
return False
system = dbapi.isystem_get_one()
return system.capabilities.get('https_enabled', False)

View File

@ -1211,7 +1211,6 @@ def create_test_interface(**kw):
:param kw: kwargs with overriding values for interface's attributes.
:returns: Test Interface DB object.
"""
interface = get_test_interface(**kw)
datanetworks_list = interface.get('datanetworks') or []
networks_list = interface.get('networks') or []
@ -1264,6 +1263,20 @@ def create_test_interface_network(**kw):
return dbapi.interface_network_create(interface_network)
def create_test_interface_network_assign(interface_id, network_id):
"""Create test network interface entry in DB and return Network DB
object. Function to be used to create test Network objects in the database.
:param interface_id: interface object id.
:param network_id: interface object id.
:returns: Test Network DB object.
"""
dbapi = db_api.get_instance()
net = dbapi.network_get(network_id)
values = {'interface_id': interface_id,
'network_id': net.id}
return dbapi.interface_network_create(values)
def get_test_interface_network(**kw):
inv = {
'id': kw.get('id'),

File diff suppressed because it is too large Load Diff