Page MenuHomeDevCentral

D4033.id10548.diff
No OneTemporary

D4033.id10548.diff

diff --git a/roles/router/carp/files/carp-ovh-failover b/roles/router/carp/files/carp-ovh-failover
new file mode 100644
--- /dev/null
+++ b/roles/router/carp/files/carp-ovh-failover
@@ -0,0 +1,242 @@
+#!/usr/local/bin/python3
+
+# -------------------------------------------------------------
+# Network — CARP OVH failover python script
+# -------------------------------------------------------------
+# Project: Nasqueron
+# License: Trivial work, not eligible to copyright
+# Source file: roles/router/carp/files/carp-ovh-failover
+# -------------------------------------------------------------
+#
+# <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>
+
+import ovh
+import secretsmith
+from secretsmith.vault import secrets
+import sys
+import time
+import subprocess
+import socket
+import yaml
+
+
+# ---------------- CONFIG ----------------
+
+
+with open("/usr/local/libexec/carp/config.yaml", "r") as f:
+ config = yaml.safe_load(f)
+
+SERVICE = config['ovh']["service"]
+
+VIP = config['ovh']["vip"]
+
+MAC_TO_ROUTER = config['routers']["mac_to_router"]
+
+VAULT_CONFIG = config['vault']["config"]
+
+VM_NAME = socket.gethostname()
+
+
+# ---------------- HELPER FUNCTIONS -----------------------------
+
+
+def log(message):
+ """
+ Log a message both to the system logs.
+
+ This is useful because the script is triggered by devd,
+ so messages need to be visible in /var/log/messages for debugging.
+ """
+ subprocess.run(["logger", "-t", "carp-ovh", message])
+
+
+def get_my_mac(interface):
+ """
+ Retrieve the MAC address of the interface passed by devd.
+ """
+ output = subprocess.check_output(["ifconfig", interface]).decode()
+
+ for line in output.splitlines():
+ if "ether" in line:
+ mac = line.split()[1]
+ log(f"Detected MAC on {interface}: {mac}")
+ return mac
+
+ raise Exception(f"MAC address not found on interface {interface}")
+
+
+def build_ovh_client():
+ """
+ Build an OVH API client from credentials stored in Vault.
+ """
+ vault_client = secretsmith.login(config_path=VAULT_CONFIG)
+ secret = secrets.read_secret(vault_client, "apps", "network/carp-hyper-001-switch")
+
+ return ovh.Client(
+ endpoint="ovh-eu",
+ application_key=secret["application_key"],
+ application_secret=secret["application_secret"],
+ consumer_key=secret["consumer_key"],
+ )
+
+
+def get_ips(client,mac):
+ """
+ Get the list of IPs associated with a MAC via the OVH API.
+ Used to check if the VIP is already assigned.
+ """
+ url = f"/dedicated/server/{SERVICE}/virtualMac/{mac}/virtualAddress"
+
+ log(f"Checking IPs for MAC {mac}")
+
+ try:
+ result = client.get(url)
+ log(f"OVH returned: {result}")
+ return result
+ except Exception as e:
+ log(f"Error in get_ips for {mac}: {repr(e)}")
+ raise
+
+
+def delete_vip(client,mac):
+ """
+ Delete the VIP from a given MAC address on OVH.
+ """
+ log(f"Deleting VIP from {mac}")
+
+ client.delete( f"/dedicated/server/{SERVICE}/virtualMac/{mac}/virtualAddress/{VIP}")
+
+
+def add_vip(client,mac):
+ """
+ Add the VIP to a given MAC address on OVH.
+ """
+ log(f"Adding VIP to {mac}")
+ client.post(
+ f"/dedicated/server/{SERVICE}/virtualMac/{mac}/virtualAddress",
+ ipAddress=VIP,
+ virtualMachineName=VM_NAME,
+ )
+
+
+# ---------------- MAIN FUNCTION -------------------------------
+
+
+def run(interface, state):
+ """
+ Handle CARP failover with OVH:
+ move VIP from other MAC to MY_MAC when becoming MASTER.
+ """
+ # Only act when the router becomes MASTER
+ # We don't want to modify the OVH configuration when the node is BACKUP,
+ # otherwise both routers could try to assign the VIP at the same time.
+ if state != "MASTER":
+ log("Not MASTER -> exit")
+ return
+
+ client = build_ovh_client()
+
+ MY_MAC = get_my_mac(interface)
+
+ OTHER_MAC = [mac for mac in MAC_TO_ROUTER if mac != MY_MAC][0]
+
+ log("Checking current state...")
+
+ # Step 0: already correct?
+ if VIP in get_ips(client,MY_MAC):
+ log("VIP is already on correct MAC -> nothing to do")
+ return
+
+ # Step 1: delete old mapping if VIP is on the other MAC
+ if VIP in get_ips(client,OTHER_MAC):
+ try:
+ delete_vip(client,OTHER_MAC)
+ log("DELETE request accepted")
+ except Exception as e:
+ log(f"ERROR during DELETE: {e}")
+ raise
+
+ # Step 2: wait until deletion is effective (exponential backoff)
+ delay = 1
+ total_wait = 0
+ warned = False
+
+ while True:
+ log(f"Waiting deletion... (sleep {delay}s)")
+ time.sleep(delay)
+ total_wait += delay
+
+ ips = get_ips(client,OTHER_MAC)
+
+ if VIP not in ips:
+ log(f"Deletion confirmed: VIP removed from {OTHER_MAC}")
+ break
+
+ # warning after 128 (2m08) seconds
+ if total_wait >= 128 and not warned:
+ log(f"WARNING: VIP still present on {OTHER_MAC} after {total_wait}s")
+ warned = True
+
+ # exponential backoff, capped at 60s to avoid waiting too long between checks
+ delay = min(delay * 2, 60)
+
+ # Step 3: add VIP on MY_MAC
+ try:
+ add_vip(client,MY_MAC)
+ log("ADD request accepted")
+ except Exception as e:
+ log(f"ERROR during ADD: {e}")
+ raise
+
+ # Step 4: wait until addition is effective (exponential backoff)
+ delay = 1
+ total_wait = 0
+ warned = False
+
+ while True:
+ log(f"Checking add... (sleep {delay}s)")
+ time.sleep(delay)
+ total_wait += delay
+
+ ips = get_ips(client,MY_MAC)
+
+ if VIP in ips:
+ log(f"Addition confirmed: VIP attached to {MY_MAC}")
+ break
+
+ # warning after 128 seconds (2m08)
+ if total_wait >= 128 and not warned:
+ log(f"WARNING: VIP still not attached to {MY_MAC} after {total_wait}s")
+ warned = True
+
+ # exponential backoff, capped at 60s to avoid waiting too long between checks
+ delay = min(delay * 2, 60)
+
+ log("Script finished successfully")
+
+
+# -------------------------------------------------------------
+# Application entry point
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 3:
+ print("Usage: script <subsystem> <state>")
+ sys.exit(1)
+
+ subsystem = sys.argv[1]
+ state = sys.argv[2]
+
+ try:
+ interface = subsystem.split("@", 1)[1]
+ except IndexError:
+ print("Bad subsystem format")
+ sys.exit(2)
+
+ run(interface, state)
diff --git a/roles/router/carp/files/config.yaml b/roles/router/carp/files/config.yaml
new file mode 100644
--- /dev/null
+++ b/roles/router/carp/files/config.yaml
@@ -0,0 +1,26 @@
+# -------------------------------------------------------------
+# Network — CARP OVH VAULT configuration
+# -------------------------------------------------------------
+# Project: Nasqueron
+# License: Trivial work, not eligible to copyright
+# Source file: roles/router/carp/files/config.yaml
+# -------------------------------------------------------------
+#
+# <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>
+
+ovh:
+ service: ns3173530.ip-51-210-99.eu
+ vip: "51.68.252.230"
+
+routers:
+ mac_to_router:
+ "00:50:56:09:3c:f2": router-002
+ "00:50:56:09:98:fc": router-003
+
+vault:
+ config: /usr/local/etc/secrets/carp-secretsmith.yaml
diff --git a/roles/router/carp/init.sls b/roles/router/carp/init.sls
--- a/roles/router/carp/init.sls
+++ b/roles/router/carp/init.sls
@@ -40,3 +40,15 @@
vault:
approle: {{ salt["credentials.read_secret"]("network/router/vault") }}
addr: {{ pillar["nasqueron_services"]["vault_url"] }}
+
+/usr/local/libexec/carp/config.yaml:
+ file.managed:
+ - source: salt://roles/router/carp/files/config.yaml
+ - makedirs: True
+ - mode: 644
+
+/usr/local/libexec/carp/carp-ovh-failover:
+ file.managed:
+ - source: salt://roles/router/carp/files/carp-ovh-failover
+ - makedirs: True
+ - mode: 755

File Metadata

Mime Type
text/plain
Expires
Sun, Apr 5, 06:52 (16 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3595192
Default Alt Text
D4033.id10548.diff (8 KB)

Event Timeline