diff --git a/_modules/node.py b/_modules/node.py index 28fc3c9..4ec9b53 100644 --- a/_modules/node.py +++ b/_modules/node.py @@ -1,261 +1,317 @@ # -*- coding: utf-8 -*- # ------------------------------------------------------------- # Salt — Node execution module # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Project: Nasqueron # Created: 2017-10-21 # Description: Functions related to the nodes' pillar entry # License: BSD-2-Clause # ------------------------------------------------------------- from salt.exceptions import CommandExecutionError, SaltCloudConfigError from salt._compat import ipaddress def _get_all_nodes(): return __pillar__.get("nodes", {}) def get_all_properties(nodename=None): """ A function to get a node pillar configuration. CLI Example: salt * node.get_all_properties """ if nodename is None: nodename = __grains__["id"] all_nodes = _get_all_nodes() if nodename not in all_nodes: raise CommandExecutionError( SaltCloudConfigError("Node {0} not declared in pillar.".format(nodename)) ) return all_nodes[nodename] def get(key, nodename=None): """ A function to get a node pillar configuration key. CLI Example: salt * node.get hostname """ return _get_property(key, nodename, None) def _explode_key(k): return k.split(":") def _get_first_key(k): return _explode_key(k)[0] def _strip_first_key(k): return ":".join(_explode_key(k)[1:]) def _get_property(key, nodename, default_value, parent=None): if parent is None: parent = get_all_properties(nodename) if ":" in key: first_key = _get_first_key(key) if first_key in parent: return _get_property( _strip_first_key(key), nodename, default_value, parent[first_key] ) elif key in parent: return parent[key] return default_value def get_list(key, nodename=None): """ A function to get a node pillar configuration. Returns a list if found, or an empty list if not found. CLI Example: salt * node.list network:ipv4_aliases """ return _get_property(key, nodename, []) def has(key, nodename=None): """ A function to get a node pillar configuration. Returns a boolean, False if not found. CLI Example: salt * node.has network:ipv6_tunnel """ value = _get_property(key, nodename, False) return bool(value) def has_role(role, nodename=None): """ A function to determine if a node has the specified role. Returns a boolean, False if not found. CLI Example: salt * node.has_role devserver """ return role in get_list("roles", nodename) def filter_by_role(pillar_key, nodename=None): """ A function to filter a dictionary by roles. The dictionary must respect the following structure: - keys are role to check the current node against - values are list of items If a key '*' is also present, it will be included for every role. Returns a list, extending all the filtered lists. CLI Example: salt * node.filter_by_role web_content_sls """ roles = get_list("roles", nodename) dictionary = __pillar__.get(pillar_key, {}) filtered_list = [] for role, items in dictionary.items(): if role == "*" or role in roles: filtered_list.extend(items) return filtered_list def filter_by_name(pillar_key, nodename=None): """ A function to filter a dictionary by node name. The dictionary must respect the following structure: - keys are names to check the current node against - values are list of items If a key '*' is also present, it will be included for every node. Returns a list, extending all the filtered lists. CLI Example: salt * node.filter_by_name mars """ if nodename is None: nodename = __grains__["id"] dictionary = __pillar__.get(pillar_key, {}) filtered_list = [] for name, items in dictionary.items(): if name == "*" or name == nodename: filtered_list.extend(items) return filtered_list def has_web_content(content, nodename=None): return content in filter_by_role("web_content_sls", nodename) def get_wwwroot(nodename=None): """ A function to determine the wwwroot folder to use. Returns a string depending on the FQDN. CLI Example: salt * node.get_wwwroot """ hostname = _get_property("hostname", nodename, None) if hostname is None: raise CommandExecutionError( SaltCloudConfigError( "Node {0} doesn't have a hostname property".format(nodename) ) ) if hostname.count(".") < 2: return "wwwroot/{0}/www".format(hostname) fqdn = hostname.split(".") return "wwwroot/{1}/{0}".format(".".join(fqdn[0:-2]), ".".join(fqdn[-2:])) def get_ipv6_list(): """ A function to get a list of IPv6, enclosed by []. Returns a string depending on the IPv6 currently assigned. CLI Example: salt * node.get_ipv6_list """ ipv6 = __grains__.get("ipv6") return " ".join(["[" + ip + "]" for ip in ipv6]) def resolve_network(): """ A function to determine canonical properties of networks from the nodes pillar. CLI Example: salt * node.resolve_network """ network = { "ipv4_address": "", "ipv4_gateway": "", } private_network = network.copy() interfaces = _get_property("network:interfaces", __grains__["id"], {}) for interface_name, interface in interfaces.items(): if "ipv4" not in interface: continue ipv4 = interface["ipv4"]["address"] if ipaddress.ip_address(ipv4).is_private: target = private_network else: target = network if target["ipv4_address"] != "": continue target["ipv4_address"] = ipv4 try: target["ipv4_gateway"] = interface["ipv4"]["gateway"] except KeyError: pass if network["ipv4_address"] == "": return private_network return network + + +def _resolve_gre_tunnels_for_router(network, netmask): + tunnels = [] + + for node, tunnel in __pillar__.get(f"{network}_gre_tunnels", {}).items(): + tunnels.append( + { + "description": f"{network}_to_{node}", + "interface": tunnel["router"]["interface"], + "src": tunnel["router"]["addr"], + "dst": tunnel["node"]["addr"], + "netmask": netmask, + "icann_src": get("network")["canonical_public_ipv4"], + "icann_dst": get("network", node)["canonical_public_ipv4"], + } + ) + + return tunnels + + +def resolve_gre_tunnels(): + """ + A function to get the GRE tunnels for a node + + CLI Example: + salt * node.resolve_gre_tunnels + """ + gre_tunnels = [] + + for network, network_args in __pillar__.get("networks", {}).items(): + if __grains__["id"] == network_args["router"]: + gre_tunnels += _resolve_gre_tunnels_for_router( + network, network_args["netmask"] + ) + continue + + tunnel = __salt__["pillar.get"](f"{network}_gre_tunnels:{__grains__['id']}") + if not tunnel: + continue + + gre_tunnels.append( + { + "description": f"{network}_via_{network_args['router']}", + "interface": tunnel["node"].get("interface", "gre0"), + "src": tunnel["node"]["addr"], + "dst": tunnel["router"]["addr"], + "netmask": network_args["netmask"], + "icann_src": get("network")["canonical_public_ipv4"], + "icann_dst": get("network", network_args["router"])[ + "canonical_public_ipv4" + ], + } + ) + + return gre_tunnels diff --git a/pillar/core/network.sls b/pillar/core/network.sls index 8afb63c..ab5e9b8 100644 --- a/pillar/core/network.sls +++ b/pillar/core/network.sls @@ -1,29 +1,49 @@ networks: drake: netmask: 255.255.255.0 - addr: - cloudhugger: 172.27.27.28 - router-001: 172.27.27.1 - windriver: 172.27.27.35 - ysul: 172.27.27.33 + router: router-001 -gre_tunnels: - windriver: - wind-cloud: - interface: gre0 - network: drake - to: cloudhugger - - wind-ysul: - interface: gre0 - network: drake - to: ysul +# ------------------------------------------------------------- +# Drake - GRE tunnels +# +# Describe GRE tunnels between a node and {networks.drake.router} +# +# {id}: +# router: +# interface: gre0, gre1, ... / increment for each tunnel +# addr: The tunnel IPv4 on the router in 172.27.27.240/28 +# +# node: +# interface: Not needed on Linux as the interface name will be +# descriptive: gre-drake_via_{networks.drake.router} +# +# Not needed on FreeBSD if default value "gre0" is fine. +# If there is several GRE tunnels, can be gre1, gre2, etc. +# +# addr: The canonical IPv4 for the server in 172.27.27.0/24 +# +# IP should be sync between the pillar and the Operations grimoire +# at https://agora.nasqueron.org/Operations_grimoire/Network +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +drake_gre_tunnels: ysul: - wind-ysul: &gre_drake_to_windriver + router: interface: gre0 - network: drake - to: windriver + addr: 172.27.27.252 + node: + addr: 172.27.27.33 cloudhugger: - wind-cloud: *gre_drake_to_windriver + router: + interface: gre1 + addr: 172.27.27.253 + node: + addr: 172.27.27.28 + + windriver: + router: + interface: gre2 + addr: 172.27.27.254 + node: + addr: 172.27.27.35 diff --git a/roles/core/network/gre.sls b/roles/core/network/gre.sls index 0a6dd6f..2738cad 100644 --- a/roles/core/network/gre.sls +++ b/roles/core/network/gre.sls @@ -1,79 +1,54 @@ # ------------------------------------------------------------- # Salt — Network — GRE tunnels # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Project: Nasqueron # Created: 2020-09-20 # License: Trivial work, not eligible to copyright # ------------------------------------------------------------- -{% set network = salt['node.get']('network') %} -{% set gre_tunnels = salt['pillar.get']("gre_tunnels:" + grains['id'], {}) %} +{% from "roles/core/network/map.jinja" import gre with context %} {% set boot_loader = namespace(gre=false) %} # ------------------------------------------------------------- # Tunnels network configuration files # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{% for description, tunnel in gre_tunnels.items() %} +{% for tunnel in salt['node.resolve_gre_tunnels']() %} {% set boot_loader.gre = True %} -{% set tunnel_network = pillar['networks'][tunnel['network']] %} -{% if grains['os'] == 'FreeBSD' %} -/etc/rc.conf.d/netif/gre_{{ description }}: +{{ gre.config_path }}{{ tunnel["description"] }}: file.managed: - - source: salt://roles/core/network/files/FreeBSD/netif_gre.rc + - source: salt://roles/core/network/files/{{ gre.source_path }} - makedirs: True - template: jinja - - context: - description: {{ description }} - interface: {{ tunnel['interface'] }} - - src: {{ tunnel_network['addr'][grains['id']] }} - dst: {{ tunnel_network['addr'][tunnel['to']] }} - - icann_src: {{ network['ipv4_address'] }} - icann_dst: {{ salt['node.get']('network', tunnel['to'])['ipv4_address'] }} -{% endif %} - + - defaults: {{ tunnel }} {% if grains['os_family'] == 'Debian' %} -/etc/network/interfaces.d/10-gre-{{ description }}: - file.managed: - - source: salt://roles/core/network/files/Debian/10-gre.jinja - - makedirs: True - - template: jinja - context: interface: gre-{{ description }} - - src: {{ tunnel_network['addr'][grains['id']] }} - dst: {{ tunnel_network['addr'][tunnel['to']] }} - netmask: {{ tunnel_network['netmask'] }} - - icann_src: {{ network['ipv4_address'] }} - icann_dst: {{ salt['node.get']('network', tunnel['to'])['ipv4_address'] }} {% endif %} {% endfor %} # ------------------------------------------------------------- # Kernel configuration # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if boot_loader.gre %} {% if grains['os'] == 'FreeBSD' %} load_gre_kernel_module: file.append: - name: /boot/loader.conf - text: | if_gre_load="YES" {% endif %} {% if grains['os_family'] == 'Debian' %} ip_gre: kmod.present: - persist: True {% endif %} {% endif %} diff --git a/roles/core/network/map.jinja b/roles/core/network/map.jinja index 6cb6f46..5961208 100644 --- a/roles/core/network/map.jinja +++ b/roles/core/network/map.jinja @@ -1,34 +1,51 @@ # ------------------------------------------------------------- # Salt — Network # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Project: Nasqueron # License: Trivial work, not eligible to copyright # ------------------------------------------------------------- # ------------------------------------------------------------- # Interface configuration by OS/distro # # config_path: the configuration file to write in OS # source_path: in this repo, roles/core/network/files/<source_path> # # Don't set default value, so we MUST define them # for EACH os/distro. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% set interface_config = salt['grains.filter_by']({ 'FreeBSD': { "config_path": "/etc/rc.conf.d/netif/ipv4_", "source_path": "FreeBSD/netif_ipv4.rc", "suffix": "interface", }, 'RedHat': { "config_path": "/etc/sysconfig/network-scripts/ifcfg-", "source_path": "RedHat/ifcfg", "suffix": "device", }, 'Debian': { "config_path": "/etc/network/interfaces.d/10-net-", "source_path": "Debian/10-net.jinja", "suffix": "device", }, }) %} + +# ------------------------------------------------------------- +# GRE tunnels configuration by OS/distro +# +# See interface configuration for the documentation. +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +{% set gre = salt['grains.filter_by']({ + 'FreeBSD': { + "config_path": "/etc/rc.conf.d/netif/gre_", + "source_path": "FreeBSD/netif_gre.rc", + }, + 'Debian': { + "config_path": "/etc/network/interfaces.d/10-gre-", + "source_path": "Debian/10-gre.jinja", + }, +}) %}