Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3766230
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/_modules/node.py b/_modules/node.py
index c8cdb43..44d95dd 100644
--- a/_modules/node.py
+++ b/_modules/node.py
@@ -1,460 +1,482 @@
# -*- 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
DEPLOY_ROLES = [
"devserver",
"salt-primary",
"viperserv",
"webserver-alkane",
"webserver-legacy",
]
WITH_NGINX_ROLES = [
"webserver-alkane",
"webserver-core",
"paas-docker",
]
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_deployment(nodename=None):
"""
A function to determine if this server does continuous delivery.
"""
return any(role in DEPLOY_ROLES for role in get_list("roles", nodename))
def has_nginx(nodename=None):
"""
A function to determine if this server role should include nginx.
"""
return any(role in WITH_NGINX_ROLES for role in get_list("roles", 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 has_interface_flag(flag, nodename=None):
interfaces = _get_property("network:interfaces", nodename, None)
return any(
[
flag in interface["flags"]
for interface in interfaces.values()
if "flags" in interface
]
)
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 get_all_ips():
+ """
+ A function to get a list of IPv4, not enclosed,
+ and IPv6, enclosed by [].
+ Returns a string depending on the IPv6 currently assigned.
+
+ CLI Example:
+
+ salt * node.get_all_ips
+ """
+ all_ips = []
+
+ for _interface, ips in __grains__.get("ip4_interfaces").items():
+ all_ips.extend(ips)
+
+ for _interface, ips in __grains__.get("ip6_interfaces").items():
+ ips = ["[" + ip + "]" for ip in ips]
+ all_ips.extend(ips)
+
+ return " ".join(set(all_ips))
+
+
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()
is_private_network_stable = True
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"] == "":
main_network = private_network
else:
main_network = network
if private_network["ipv4_address"] == "":
is_private_network_stable = False
tunnels = resolve_gre_tunnels()
if tunnels:
tunnel = tunnels[0]
private_network = {
"ipv4_address": tunnel["src"],
"ipv4_gateway": tunnel["gateway"],
}
return main_network | {
"private_ipv4_address": private_network["ipv4_address"],
"private_ipv4_gateway": private_network["ipv4_gateway"],
"is_private_network_stable": is_private_network_stable,
}
def _resolve_gre_tunnels_for_router(network, netmask):
tunnels = []
for node, tunnel in __pillar__.get(f"{network}_gre_tunnels", {}).items():
tunnels.append(
{
"network": network,
"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(
{
"network": network,
"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"],
"gateway": network_args["default_gateway"],
"icann_src": get("network")["canonical_public_ipv4"],
"icann_dst": get("network", network_args["router"])[
"canonical_public_ipv4"
],
}
)
return gre_tunnels
def get_gateway(network):
# For tunnels, gateway is the tunnel endpoint
tunnel = __salt__["pillar.get"](f"{network}_gre_tunnels:{__grains__['id']}")
if tunnel:
return tunnel["router"]["addr"]
return __salt__["pillar.get"](f"networks:{network}:default_gateway")
def _get_static_route(cidr, gateway):
if __grains__["os_family"] == "FreeBSD":
return f"-net {cidr} {gateway}"
if __grains__["kernel"] == "Linux":
return f"{cidr} via {gateway}"
raise ValueError("No static route implementation for " + __grains__["os_family"])
def _get_default_route(gateway):
if __grains__["os_family"] == "FreeBSD":
return f"default {gateway}"
if __grains__["kernel"] == "Linux":
return f"default via {gateway}"
raise ValueError("No static route implementation for " + __grains__["os_family"])
def _get_interface_route(ip, interface):
if __grains__["os_family"] == "FreeBSD":
return f"-net {ip}/32 -interface {interface}"
if __grains__["kernel"] == "Linux":
return f"{ip} dev {interface}"
raise ValueError("No static route implementation for " + __grains__["os_family"])
def _get_routes_for_private_networks():
"""
Every node, excepted the routeur, should have a route
for the private network CIDR to the router.
For GRE tunnels, the gateway is the tunnel endpoint.
In other cases, the gateway is the main router (private) IP.
"""
routes = {}
for network, network_args in __pillar__.get("networks", {}).items():
if network_args["router"] == __grains__["id"]:
continue
gateway = get_gateway(network)
routes[f"private_{network}"] = _get_static_route(network_args["cidr"], gateway)
return routes
def get_routes():
routes = {}
interfaces = _get_property("network:interfaces", __grains__["id"], {})
for interface_name, interface in interfaces.items():
flags = interface.get("flags", [])
if "gateway" in interface.get("ipv4", {}):
gateway = interface["ipv4"]["gateway"]
if "ipv4_ovh_failover" in flags:
routes[f"{interface_name}_gateway"] = _get_interface_route(
gateway, interface["device"]
)
if __grains__["os_family"] != "RedHat":
# On RHEL/CentOS/Rocky, legacy network scripts take care of this with GATEWAY=
routes[f"{interface_name}_default"] = _get_default_route(gateway)
routes.update(_get_routes_for_private_networks())
return routes
diff --git a/roles/devserver/init.sls b/roles/devserver/init.sls
index dc23ce4..88f7de0 100644
--- a/roles/devserver/init.sls
+++ b/roles/devserver/init.sls
@@ -1,16 +1,17 @@
# -------------------------------------------------------------
# Salt — Provision a development server
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Project: Nasqueron
# Created: 2017-10-20
# License: Trivial work, not eligible to copyright
# -------------------------------------------------------------
include:
- .datacube
- .dns
- .mail
- .pkg
- .userland-software
- .userland-home
+ - .webserver-home
- .webserver-wwwroot51
diff --git a/roles/devserver/webserver-home/files/001-server.conf b/roles/devserver/webserver-home/files/001-server.conf
new file mode 100644
index 0000000..17cc7c6
--- /dev/null
+++ b/roles/devserver/webserver-home/files/001-server.conf
@@ -0,0 +1,65 @@
+# -------------------------------------------------------------
+# Nginx :: server homepage and home directories
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: Trivial work, not eligible to copyright
+# Source file: roles/devserver/webserver-home/files/001-server.conf
+# -------------------------------------------------------------
+#
+# <auto-generated>
+# This file is managed by our rOPS SaltStack repository.
+#
+# Changes to this file may cause incorrect behavior
+# and will be lost if the state is redeployed.
+# </auto-generated>
+
+ server {
+ listen 80;
+ listen [::]:80;
+ server_name {{ fqdn }} {{ hostname }}.nasqueron.drake localhost {{ ips }};
+
+ include includes/tls;
+ ssl_certificate /usr/local/etc/letsencrypt/live/{{ fqdn }}/fullchain.pem;
+ ssl_certificate_key /usr/local/etc/letsencrypt/live/{{ fqdn }}/privkey.pem;
+
+ include includes/letsencrypt;
+
+ ###
+ ### Cover pages
+ ###
+
+ root /var/wwwroot/nasqueron.org/{{ hostname }};
+ index index.html index.htm;
+
+ ###
+ ### API
+ ###
+
+ location ~ ^/datasources/.*\.json(/|$) {
+ include includes/cors-open;
+ }
+
+ ###
+ ### public_html user directories
+ ###
+
+ set $userdir public_html;
+
+ location ~ ^/~(.+?)(/.*)?$ {
+ alias /var/home-wwwroot/$1$2;
+ index index.html index.htm;
+
+ autoindex on;
+ charset utf-8;
+ }
+
+ ###
+ ### Misc directories
+ ###
+
+ location /poudriere {
+ alias /usr/local/poudriere/data/logs/bulk;
+ autoindex on;
+ }
+
+ }
diff --git a/roles/devserver/webserver-home/files/setup-web-home.py b/roles/devserver/webserver-home/files/setup-web-home.py
new file mode 100755
index 0000000..b90356b
--- /dev/null
+++ b/roles/devserver/webserver-home/files/setup-web-home.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+
+# -------------------------------------------------------------
+# Setup web home
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Create folder and symlink for public_html
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import os
+import shutil
+import sys
+
+
+# -------------------------------------------------------------
+# Web setup
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def is_clean(username):
+ return not os.path.exists(f"/var/home-wwwroot/{username}") and not os.path.exists(
+ f"/home/{username}/public_html"
+ )
+
+
+def is_valid_setup(username):
+ return (
+ os.path.exists(f"/var/home-wwwroot/{username}")
+ and not os.path.islink(f"/var/home-wwwroot/{username}")
+ and os.path.islink(f"/home/{username}/public_html")
+ and os.readlink(f"/home/{username}/public_html")
+ == f"/var/home-wwwroot/{username}"
+ )
+
+
+def setup(username):
+ os.mkdir(f"/var/home-wwwroot/{username}", mode=0o755)
+ shutil.chown(f"/var/home-wwwroot/{username}", user=username, group="web")
+ os.symlink(f"/var/home-wwwroot/{username}", f"/home/{username}/public_html")
+
+
+# -------------------------------------------------------------
+# Application entry point
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def run(username):
+ if is_valid_setup(username):
+ print("Setup is already done and looks correct.", file=sys.stderr)
+ sys.exit(4)
+
+ if not is_clean(username):
+ print(
+ "Directories exist but aren't correct, check them manually.",
+ file=sys.stderr,
+ )
+ sys.exit(2)
+
+ try:
+ setup(username)
+ except OSError as e:
+ print(e, file=sys.stderr)
+ sys.exit(8)
+
+
+if __name__ == "__main__":
+ argc = len(sys.argv)
+
+ if argc < 2:
+ print(f"Usage: {sys.argv[0]} <username>", file=sys.stderr)
+ sys.exit(1)
+
+ run(sys.argv[1])
diff --git a/roles/devserver/webserver-home/init.sls b/roles/devserver/webserver-home/init.sls
new file mode 100644
index 0000000..aad77d0
--- /dev/null
+++ b/roles/devserver/webserver-home/init.sls
@@ -0,0 +1,36 @@
+# -------------------------------------------------------------
+# Salt — Provision a development server
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: Trivial work, not eligible to copyright
+# -------------------------------------------------------------
+
+{% from "map.jinja" import dirs with context %}
+
+# -------------------------------------------------------------
+# Home directories
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/var/home-wwwroot:
+ file.directory:
+ - mode: 711
+ - group: web
+
+{{ dirs.bin }}/setup-web-home:
+ file.managed:
+ - source: salt://roles/devserver/webserver-home/files/setup-web-home.py
+ - mode: 755
+
+# -------------------------------------------------------------
+# Default vhost
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+{{ dirs.etc }}/nginx/vhosts/001-server.conf:
+ file.managed:
+ - source: salt://roles/devserver/webserver-home/files/001-server.conf
+ - mode: 644
+ - template: jinja
+ - context:
+ hostname: {{ grains.host }}
+ fqdn: {{ grains.fqdn }}
+ ips: "{{ salt["node.get_all_ips"]() }}"
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 24, 17:12 (33 m, 44 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2258502
Default Alt Text
(19 KB)
Attached To
Mode
rOPS Nasqueron Operations
Attached
Detach File
Event Timeline
Log In to Comment