Prepara l'architettura multi-nodo (MediaMTX+ffmpeg) con assignment URL per sessione, admin di provision/drain e provider Cloud/DNS astratti verso mltv-stream.net. Co-authored-by: Cursor <cursoragent@cursor.com>
106 lines
2.9 KiB
Ruby
106 lines
2.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "faraday"
|
|
require "cgi"
|
|
|
|
module Streams
|
|
module DnsProviders
|
|
# Hetzner Cloud DNS (Console) — zone mltv-stream.net.
|
|
#
|
|
# ENV:
|
|
# HCLOUD_TOKEN (stesso del Cloud)
|
|
# STREAM_DNS_ZONE (default mltv-stream.net)
|
|
# STREAM_DNS_TTL (default 60)
|
|
class Hetzner < Base
|
|
API = "https://api.hetzner.cloud/v1"
|
|
|
|
def initialize(token: ENV.fetch("HCLOUD_TOKEN"), zone: nil, conn: nil)
|
|
@token = token
|
|
@zone = zone || ENV.fetch("STREAM_DNS_ZONE", "mltv-stream.net")
|
|
@ttl = ENV.fetch("STREAM_DNS_TTL", "60").to_i
|
|
@conn = conn
|
|
end
|
|
|
|
def upsert_a(name, ip)
|
|
rr_name = relative_name(name)
|
|
delete_a(name)
|
|
post("/zones/#{CGI.escape(@zone)}/rrsets", {
|
|
name: rr_name,
|
|
type: "A",
|
|
ttl: @ttl,
|
|
records: [{ value: ip.to_s, comment: "matchlivetv stream-node" }],
|
|
labels: { "matchlivetv" => "true", "role" => "stream-node" }
|
|
})
|
|
true
|
|
end
|
|
|
|
def delete_a(name)
|
|
rr_name = relative_name(name)
|
|
encoded = CGI.escape(rr_name)
|
|
response = conn.delete("/zones/#{CGI.escape(@zone)}/rrsets/#{encoded}/A") do |req|
|
|
req.headers.update(auth_headers)
|
|
end
|
|
return true if response.status == 404 || response.status == 204 || response.success?
|
|
|
|
raise Error, "Hetzner DNS API #{response.status}: #{response.body.inspect}"
|
|
end
|
|
|
|
def resolve(name)
|
|
rr_name = relative_name(name)
|
|
data = get("/zones/#{CGI.escape(@zone)}/rrsets", name: rr_name, type: "A")
|
|
rrset = Array(data["rrsets"]).first
|
|
Array(rrset&.dig("records")).first&.dig("value")
|
|
rescue Error
|
|
nil
|
|
end
|
|
|
|
private
|
|
|
|
def relative_name(name)
|
|
host = name.to_s.strip.downcase.delete_suffix(".")
|
|
suffix = ".#{@zone}"
|
|
return "@" if host == @zone
|
|
return host.delete_suffix(suffix) if host.end_with?(suffix)
|
|
|
|
host
|
|
end
|
|
|
|
def conn
|
|
@conn ||= Faraday.new(url: API) do |f|
|
|
f.request :json
|
|
f.response :json, content_type: /\bjson$/
|
|
f.adapter Faraday.default_adapter
|
|
end
|
|
end
|
|
|
|
def auth_headers
|
|
{ "Authorization" => "Bearer #{@token}" }
|
|
end
|
|
|
|
def get(path, params = {})
|
|
response = conn.get(path) do |req|
|
|
req.headers.update(auth_headers)
|
|
req.params.update(params)
|
|
end
|
|
unwrap!(response)
|
|
end
|
|
|
|
def post(path, body)
|
|
response = conn.post(path) do |req|
|
|
req.headers.update(auth_headers)
|
|
req.body = body
|
|
end
|
|
unwrap!(response)
|
|
end
|
|
|
|
def unwrap!(response)
|
|
unless response.success?
|
|
raise Error, "Hetzner DNS API #{response.status}: #{response.body.inspect}"
|
|
end
|
|
|
|
response.body.is_a?(Hash) ? response.body : {}
|
|
end
|
|
end
|
|
end
|
|
end
|