Corregge il provisioning Hetzner: Faraday, cloud-init e default cpx12.
Sistemati path API /v1, AppArmor/auth MediaMTX sul nodo e defaults nbg1 così lo smoke Cloud è ripetibile. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,7 +15,8 @@ module Streams
|
|||||||
# HCLOUD_NETWORK_ID (opzionale, private network / WireGuard prep)
|
# HCLOUD_NETWORK_ID (opzionale, private network / WireGuard prep)
|
||||||
# HCLOUD_USER_DATA_FILE (opzionale, cloud-init path)
|
# HCLOUD_USER_DATA_FILE (opzionale, cloud-init path)
|
||||||
class Hetzner < Base
|
class Hetzner < Base
|
||||||
API = "https://api.hetzner.cloud/v1"
|
# Trailing slash obbligatorio: path assoluti tipo "/servers" altrimenti droppano /v1.
|
||||||
|
API = "https://api.hetzner.cloud/v1/"
|
||||||
|
|
||||||
def initialize(token: ENV.fetch("HCLOUD_TOKEN"), conn: nil)
|
def initialize(token: ENV.fetch("HCLOUD_TOKEN"), conn: nil)
|
||||||
@token = token
|
@token = token
|
||||||
@@ -25,12 +26,12 @@ module Streams
|
|||||||
def create_node(name:, labels: {})
|
def create_node(name:, labels: {})
|
||||||
body = {
|
body = {
|
||||||
name: name,
|
name: name,
|
||||||
server_type: ENV.fetch("HCLOUD_SERVER_TYPE", "cpx21"),
|
server_type: ENV.fetch("HCLOUD_SERVER_TYPE", "cpx12"),
|
||||||
image: ENV.fetch("HCLOUD_IMAGE", "debian-12"),
|
image: ENV.fetch("HCLOUD_IMAGE", "debian-12"),
|
||||||
location: ENV.fetch("HCLOUD_LOCATION", "fsn1"),
|
location: ENV.fetch("HCLOUD_LOCATION", "nbg1"),
|
||||||
start_after_create: true,
|
start_after_create: true,
|
||||||
labels: default_labels.merge(stringify_labels(labels)),
|
labels: default_labels.merge(stringify_labels(labels)),
|
||||||
ssh_keys: [ENV.fetch("HCLOUD_SSH_KEY", "matchlivetv-stream")],
|
ssh_keys: [ENV.fetch("HCLOUD_SSH_KEY", "matchlivetv-stream-hetzner")],
|
||||||
public_net: {
|
public_net: {
|
||||||
enable_ipv4: true,
|
enable_ipv4: true,
|
||||||
enable_ipv6: false
|
enable_ipv6: false
|
||||||
@@ -41,7 +42,7 @@ module Streams
|
|||||||
user_data = cloud_init_user_data
|
user_data = cloud_init_user_data
|
||||||
body[:user_data] = user_data if user_data.present?
|
body[:user_data] = user_data if user_data.present?
|
||||||
|
|
||||||
data = post("/servers", body)
|
data = post("servers", body)
|
||||||
server = data["server"] || {}
|
server = data["server"] || {}
|
||||||
action = data["action"]
|
action = data["action"]
|
||||||
wait_action!(action) if action
|
wait_action!(action) if action
|
||||||
@@ -51,7 +52,7 @@ module Streams
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy_node(instance_id)
|
def destroy_node(instance_id)
|
||||||
delete("/servers/#{instance_id}")
|
delete("servers/#{instance_id}")
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -61,14 +62,14 @@ module Streams
|
|||||||
params[:label_selector] = label_selector if label_selector.present?
|
params[:label_selector] = label_selector if label_selector.present?
|
||||||
params[:label_selector] ||= "matchlivetv=true,role=stream-node"
|
params[:label_selector] ||= "matchlivetv=true,role=stream-node"
|
||||||
|
|
||||||
data = get("/servers", params)
|
data = get("servers", params)
|
||||||
Array(data["servers"]).map { |s| instance_from_server(s) }
|
Array(data["servers"]).map { |s| instance_from_server(s) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_until_running(instance_id, timeout: 180)
|
def wait_until_running(instance_id, timeout: 180)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.now + timeout
|
||||||
loop do
|
loop do
|
||||||
data = get("/servers/#{instance_id}")
|
data = get("servers/#{instance_id}")
|
||||||
server = data["server"]
|
server = data["server"]
|
||||||
status = server["status"]
|
status = server["status"]
|
||||||
if status == "running"
|
if status == "running"
|
||||||
@@ -169,7 +170,7 @@ module Streams
|
|||||||
deadline = Time.now + timeout
|
deadline = Time.now + timeout
|
||||||
id = action["id"]
|
id = action["id"]
|
||||||
loop do
|
loop do
|
||||||
data = get("/actions/#{id}")
|
data = get("actions/#{id}")
|
||||||
status = data.dig("action", "status")
|
status = data.dig("action", "status")
|
||||||
return if status == "success"
|
return if status == "success"
|
||||||
raise Error, "Hetzner action #{id} failed: #{data.inspect}" if status == "error"
|
raise Error, "Hetzner action #{id} failed: #{data.inspect}" if status == "error"
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ module Streams
|
|||||||
# STREAM_DNS_ZONE (default mltv-stream.net)
|
# STREAM_DNS_ZONE (default mltv-stream.net)
|
||||||
# STREAM_DNS_TTL (default 60)
|
# STREAM_DNS_TTL (default 60)
|
||||||
class Hetzner < Base
|
class Hetzner < Base
|
||||||
API = "https://api.hetzner.cloud/v1"
|
# Trailing slash obbligatorio: path assoluti altrimenti droppano /v1.
|
||||||
|
API = "https://api.hetzner.cloud/v1/"
|
||||||
|
|
||||||
def initialize(token: ENV.fetch("HCLOUD_TOKEN"), zone: nil, conn: nil)
|
def initialize(token: ENV.fetch("HCLOUD_TOKEN"), zone: nil, conn: nil)
|
||||||
@token = token
|
@token = token
|
||||||
@@ -24,7 +25,7 @@ module Streams
|
|||||||
def upsert_a(name, ip)
|
def upsert_a(name, ip)
|
||||||
rr_name = relative_name(name)
|
rr_name = relative_name(name)
|
||||||
delete_a(name)
|
delete_a(name)
|
||||||
post("/zones/#{CGI.escape(@zone)}/rrsets", {
|
post("zones/#{CGI.escape(@zone)}/rrsets", {
|
||||||
name: rr_name,
|
name: rr_name,
|
||||||
type: "A",
|
type: "A",
|
||||||
ttl: @ttl,
|
ttl: @ttl,
|
||||||
@@ -37,7 +38,7 @@ module Streams
|
|||||||
def delete_a(name)
|
def delete_a(name)
|
||||||
rr_name = relative_name(name)
|
rr_name = relative_name(name)
|
||||||
encoded = CGI.escape(rr_name)
|
encoded = CGI.escape(rr_name)
|
||||||
response = conn.delete("/zones/#{CGI.escape(@zone)}/rrsets/#{encoded}/A") do |req|
|
response = conn.delete("zones/#{CGI.escape(@zone)}/rrsets/#{encoded}/A") do |req|
|
||||||
req.headers.update(auth_headers)
|
req.headers.update(auth_headers)
|
||||||
end
|
end
|
||||||
return true if response.status == 404 || response.status == 204 || response.success?
|
return true if response.status == 404 || response.status == 204 || response.success?
|
||||||
@@ -47,7 +48,7 @@ module Streams
|
|||||||
|
|
||||||
def resolve(name)
|
def resolve(name)
|
||||||
rr_name = relative_name(name)
|
rr_name = relative_name(name)
|
||||||
data = get("/zones/#{CGI.escape(@zone)}/rrsets", name: rr_name, type: "A")
|
data = get("zones/#{CGI.escape(@zone)}/rrsets", name: rr_name, type: "A")
|
||||||
rrset = Array(data["rrsets"]).first
|
rrset = Array(data["rrsets"]).first
|
||||||
Array(rrset&.dig("records")).first&.dig("value")
|
Array(rrset&.dig("records")).first&.dig("value")
|
||||||
rescue Error
|
rescue Error
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ RSpec.describe Streams::Autoscaler do
|
|||||||
redis.del(Streams::Autoscaler::LOCK_KEY)
|
redis.del(Streams::Autoscaler::LOCK_KEY)
|
||||||
redis.del(Streams::Autoscaler::KILL_SWITCH_KEY)
|
redis.del(Streams::Autoscaler::KILL_SWITCH_KEY)
|
||||||
redis.del(Streams::DnsProviders::Lab::REDIS_KEY)
|
redis.del(Streams::DnsProviders::Lab::REDIS_KEY)
|
||||||
|
StreamSession.where.not(stream_node_id: nil).update_all(stream_node_id: nil, status: "ended", ended_at: Time.current)
|
||||||
|
StreamNode.where.not(slug: Streams::NodeRegistry::HOME_SLUG).delete_all
|
||||||
end
|
end
|
||||||
|
|
||||||
it "is a no-op when disabled" do
|
it "is a no-op when disabled" do
|
||||||
@@ -153,7 +155,10 @@ RSpec.describe Streams::Autoscaler do
|
|||||||
"MEDIAMTX_RTMP_URL" => "rtmp://home.example:1935",
|
"MEDIAMTX_RTMP_URL" => "rtmp://home.example:1935",
|
||||||
"HLS_PUBLIC_URL" => "https://home.example/hls"
|
"HLS_PUBLIC_URL" => "https://home.example/hls"
|
||||||
) do
|
) do
|
||||||
|
StreamSession.where.not(stream_node_id: nil).update_all(stream_node_id: nil, status: "ended", ended_at: Time.current)
|
||||||
|
StreamNode.where.not(slug: Streams::NodeRegistry::HOME_SLUG).delete_all
|
||||||
home = Streams::NodeRegistry.ensure_home_from_env!
|
home = Streams::NodeRegistry.ensure_home_from_env!
|
||||||
|
home.update!(max_publishers: 1)
|
||||||
user = User.create!(email: "budget@example.com", name: "B", password: "Password123", role: "coach")
|
user = User.create!(email: "budget@example.com", name: "B", password: "Password123", role: "coach")
|
||||||
club = Club.create!(name: "Budget", sport: "volleyball")
|
club = Club.create!(name: "Budget", sport: "volleyball")
|
||||||
team = club.teams.create!(name: "T", sport: "volleyball", slug: "budget-t")
|
team = club.teams.create!(name: "T", sport: "volleyball", slug: "budget-t")
|
||||||
@@ -165,7 +170,9 @@ RSpec.describe Streams::Autoscaler do
|
|||||||
|
|
||||||
result = described_class.reconcile!(provisioner: provisioner)
|
result = described_class.reconcile!(provisioner: provisioner)
|
||||||
expect(result.actions).to include(:blocked_capacity)
|
expect(result.actions).to include(:blocked_capacity)
|
||||||
expect(described_class.metrics[:within_budget]).to eq(true)
|
# 0 overflow nodes → stima 0€ ≤ budget; il blocco è sul nodo successivo (+1)
|
||||||
|
expect(described_class.within_budget?(0)).to eq(true)
|
||||||
|
expect(described_class.within_budget?(1)).to eq(false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ require "rails_helper"
|
|||||||
RSpec.describe Streams::CloudProviders::Hetzner do
|
RSpec.describe Streams::CloudProviders::Hetzner do
|
||||||
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
|
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
|
||||||
let(:conn) do
|
let(:conn) do
|
||||||
Faraday.new(url: "https://api.hetzner.cloud/v1") do |f|
|
Faraday.new(url: Streams::CloudProviders::Hetzner::API) do |f|
|
||||||
f.request :json
|
f.request :json
|
||||||
f.response :json
|
f.response :json
|
||||||
f.adapter :test, stubs
|
f.adapter :test, stubs
|
||||||
@@ -25,15 +25,15 @@ RSpec.describe Streams::CloudProviders::Hetzner do
|
|||||||
|
|
||||||
it "creates a server and waits until running" do
|
it "creates a server and waits until running" do
|
||||||
with_env(
|
with_env(
|
||||||
"HCLOUD_LOCATION" => "fsn1",
|
"HCLOUD_LOCATION" => "nbg1",
|
||||||
"HCLOUD_SERVER_TYPE" => "cpx21",
|
"HCLOUD_SERVER_TYPE" => "cpx12",
|
||||||
"HCLOUD_IMAGE" => "debian-12",
|
"HCLOUD_IMAGE" => "debian-12",
|
||||||
"HCLOUD_SSH_KEY" => "matchlivetv-stream"
|
"HCLOUD_SSH_KEY" => "matchlivetv-stream-hetzner"
|
||||||
) do
|
) do
|
||||||
stubs.post("/servers") do |env|
|
stubs.post("https://api.hetzner.cloud/v1/servers") do |env|
|
||||||
body = JSON.parse(env.body)
|
body = JSON.parse(env.body)
|
||||||
expect(body["name"]).to eq("mltv-stream-ingest-01")
|
expect(body["name"]).to eq("mltv-stream-ingest-01")
|
||||||
expect(body["location"]).to eq("fsn1")
|
expect(body["location"]).to eq("nbg1")
|
||||||
[
|
[
|
||||||
201,
|
201,
|
||||||
{ "Content-Type" => "application/json" },
|
{ "Content-Type" => "application/json" },
|
||||||
@@ -49,10 +49,10 @@ RSpec.describe Streams::CloudProviders::Hetzner do
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
stubs.get("/actions/7") do
|
stubs.get("https://api.hetzner.cloud/v1/actions/7") do
|
||||||
[200, { "Content-Type" => "application/json" }, { "action" => { "id" => 7, "status" => "success" } }]
|
[200, { "Content-Type" => "application/json" }, { "action" => { "id" => 7, "status" => "success" } }]
|
||||||
end
|
end
|
||||||
stubs.get("/servers/42") do
|
stubs.get("https://api.hetzner.cloud/v1/servers/42") do
|
||||||
[
|
[
|
||||||
200,
|
200,
|
||||||
{ "Content-Type" => "application/json" },
|
{ "Content-Type" => "application/json" },
|
||||||
@@ -76,7 +76,7 @@ RSpec.describe Streams::CloudProviders::Hetzner do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "destroys a server" do
|
it "destroys a server" do
|
||||||
stubs.delete("/servers/42") { [204, {}, ""] }
|
stubs.delete("https://api.hetzner.cloud/v1/servers/42") { [204, {}, ""] }
|
||||||
expect(provider.destroy_node("42")).to eq(true)
|
expect(provider.destroy_node("42")).to eq(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ require "rails_helper"
|
|||||||
RSpec.describe Streams::DnsProviders::Hetzner do
|
RSpec.describe Streams::DnsProviders::Hetzner do
|
||||||
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
|
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
|
||||||
let(:conn) do
|
let(:conn) do
|
||||||
Faraday.new(url: "https://api.hetzner.cloud/v1") do |f|
|
Faraday.new(url: Streams::DnsProviders::Hetzner::API) do |f|
|
||||||
f.request :json
|
f.request :json
|
||||||
f.response :json
|
f.response :json
|
||||||
f.adapter :test, stubs
|
f.adapter :test, stubs
|
||||||
@@ -16,8 +16,8 @@ RSpec.describe Streams::DnsProviders::Hetzner do
|
|||||||
after { stubs.verify_stubbed_calls }
|
after { stubs.verify_stubbed_calls }
|
||||||
|
|
||||||
it "upserts an A record (delete then create)" do
|
it "upserts an A record (delete then create)" do
|
||||||
stubs.delete("/zones/mltv-stream.net/rrsets/ingest-01/A") { [404, {}, ""] }
|
stubs.delete("https://api.hetzner.cloud/v1/zones/mltv-stream.net/rrsets/ingest-01/A") { [404, {}, ""] }
|
||||||
stubs.post("/zones/mltv-stream.net/rrsets") do |env|
|
stubs.post("https://api.hetzner.cloud/v1/zones/mltv-stream.net/rrsets") do |env|
|
||||||
body = JSON.parse(env.body)
|
body = JSON.parse(env.body)
|
||||||
expect(body["name"]).to eq("ingest-01")
|
expect(body["name"]).to eq("ingest-01")
|
||||||
expect(body["type"]).to eq("A")
|
expect(body["type"]).to eq("A")
|
||||||
@@ -29,7 +29,7 @@ RSpec.describe Streams::DnsProviders::Hetzner do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "deletes an A record" do
|
it "deletes an A record" do
|
||||||
stubs.delete("/zones/mltv-stream.net/rrsets/ingest-01/A") { [204, {}, ""] }
|
stubs.delete("https://api.hetzner.cloud/v1/zones/mltv-stream.net/rrsets/ingest-01/A") { [204, {}, ""] }
|
||||||
expect(provider.delete_a("ingest-01.mltv-stream.net")).to eq(true)
|
expect(provider.delete_a("ingest-01.mltv-stream.net")).to eq(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ RSpec.describe Streams::NodeProvisioner do
|
|||||||
|
|
||||||
before do
|
before do
|
||||||
redis.del(Streams::DnsProviders::Lab::REDIS_KEY)
|
redis.del(Streams::DnsProviders::Lab::REDIS_KEY)
|
||||||
|
StreamSession.where.not(stream_node_id: nil).update_all(stream_node_id: nil, status: "ended", ended_at: Time.current)
|
||||||
|
StreamNode.where.not(slug: Streams::NodeRegistry::HOME_SLUG).delete_all
|
||||||
end
|
end
|
||||||
|
|
||||||
it "provisions and decommissions a simulated lab node" do
|
it "provisions and decommissions a simulated lab node" do
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ RSpec.describe Streams::NodeRegistry do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
StreamSession.where.not(stream_node_id: nil).update_all(stream_node_id: nil, status: "ended", ended_at: Time.current)
|
||||||
|
StreamNode.where.not(slug: Streams::NodeRegistry::HOME_SLUG).delete_all
|
||||||
|
StreamNode.find_by(slug: Streams::NodeRegistry::HOME_SLUG)&.update!(max_publishers: 2, status: "ready")
|
||||||
|
end
|
||||||
|
|
||||||
it "creates home node from ENV" do
|
it "creates home node from ENV" do
|
||||||
with_env(home_env) do
|
with_env(home_env) do
|
||||||
node = described_class.ensure_home_from_env!
|
node = described_class.ensure_home_from_env!
|
||||||
|
|||||||
@@ -353,10 +353,10 @@ Finché WireGuard/Tailscale non è pronto, `api_base_url` punta comunque alla pr
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
HCLOUD_TOKEN=...
|
HCLOUD_TOKEN=...
|
||||||
HCLOUD_LOCATION=fsn1
|
HCLOUD_LOCATION=nbg1
|
||||||
HCLOUD_SERVER_TYPE=cpx21
|
HCLOUD_SERVER_TYPE=cpx12
|
||||||
HCLOUD_IMAGE=debian-12
|
HCLOUD_IMAGE=debian-12
|
||||||
HCLOUD_SSH_KEY=matchlivetv-stream
|
HCLOUD_SSH_KEY=matchlivetv-stream-hetzner
|
||||||
HCLOUD_NETWORK_ID= # opzionale
|
HCLOUD_NETWORK_ID= # opzionale
|
||||||
HCLOUD_USER_DATA_FILE=/opt/matchlivetv/infra/stream-node/cloud-init.yaml
|
HCLOUD_USER_DATA_FILE=/opt/matchlivetv/infra/stream-node/cloud-init.yaml
|
||||||
|
|
||||||
|
|||||||
@@ -108,10 +108,10 @@ SENTRY_TRACES_SAMPLE_RATE=0.1
|
|||||||
# --- Stream autoscale (Hetzner Cloud + DNS mltv-stream.net) ---
|
# --- Stream autoscale (Hetzner Cloud + DNS mltv-stream.net) ---
|
||||||
# Token progetto matchlivetv-stream (Read & Write). Non committare il valore reale.
|
# Token progetto matchlivetv-stream (Read & Write). Non committare il valore reale.
|
||||||
HCLOUD_TOKEN=
|
HCLOUD_TOKEN=
|
||||||
HCLOUD_LOCATION=fsn1
|
HCLOUD_LOCATION=nbg1
|
||||||
HCLOUD_SERVER_TYPE=cpx21
|
HCLOUD_SERVER_TYPE=cpx12
|
||||||
HCLOUD_IMAGE=debian-12
|
HCLOUD_IMAGE=debian-12
|
||||||
HCLOUD_SSH_KEY=matchlivetv-stream
|
HCLOUD_SSH_KEY=matchlivetv-stream-hetzner
|
||||||
HCLOUD_NETWORK_ID=
|
HCLOUD_NETWORK_ID=
|
||||||
HCLOUD_USER_DATA_FILE=/opt/matchlivetv/infra/stream-node/cloud-init.yaml
|
HCLOUD_USER_DATA_FILE=/opt/matchlivetv/infra/stream-node/cloud-init.yaml
|
||||||
STREAM_DNS_ZONE=mltv-stream.net
|
STREAM_DNS_ZONE=mltv-stream.net
|
||||||
|
|||||||
@@ -1,41 +1,71 @@
|
|||||||
#cloud-config
|
#cloud-config
|
||||||
# Cloud-init minimale per nodo stream Hetzner (MediaMTX + ffmpeg).
|
# Nodo stream Hetzner: MediaMTX + ffmpeg.
|
||||||
# Uso in .env produzione:
|
# HCLOUD_USER_DATA_FILE=.../infra/stream-node/cloud-init.yaml
|
||||||
# HCLOUD_USER_DATA_FILE=/opt/matchlivetv/infra/stream-node/cloud-init.yaml
|
|
||||||
#
|
|
||||||
# Per boot più veloci: creare uno snapshot dopo il primo setup e usare HCLOUD_IMAGE=<snapshot>.
|
|
||||||
|
|
||||||
package_update: true
|
package_update: true
|
||||||
packages:
|
packages:
|
||||||
- docker.io
|
- docker.io
|
||||||
- ffmpeg
|
- ffmpeg
|
||||||
- curl
|
- curl
|
||||||
|
- apparmor
|
||||||
|
|
||||||
|
write_files:
|
||||||
|
- path: /opt/stream-node/mediamtx.yml
|
||||||
|
permissions: "0644"
|
||||||
|
content: |
|
||||||
|
logLevel: info
|
||||||
|
# Allinea a infra/mediamtx.yml: API raggiungibile anche da IP non-localhost
|
||||||
|
# (in prod restringere via WireGuard / firewall Hetzner).
|
||||||
|
authInternalUsers:
|
||||||
|
- user: any
|
||||||
|
pass:
|
||||||
|
ips: []
|
||||||
|
permissions:
|
||||||
|
- action: api
|
||||||
|
- action: metrics
|
||||||
|
- action: publish
|
||||||
|
- action: read
|
||||||
|
- action: playback
|
||||||
|
api: yes
|
||||||
|
apiAddress: :9997
|
||||||
|
rtmp: yes
|
||||||
|
rtmpAddress: :1935
|
||||||
|
hls: yes
|
||||||
|
hlsAddress: :8888
|
||||||
|
paths:
|
||||||
|
all_others:
|
||||||
|
- path: /opt/stream-node/bootstrap.sh
|
||||||
|
permissions: "0755"
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p /recordings /slates
|
||||||
|
systemctl enable --now docker
|
||||||
|
# aspetta il socket docker
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if docker info >/dev/null 2>&1; then break; fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
docker pull bluenviron/mediamtx:latest
|
||||||
|
docker rm -f mediamtx 2>/dev/null || true
|
||||||
|
# Debian cloud image: docker.io senza apparmor_parser → serve unconfined o pkg apparmor
|
||||||
|
docker run -d --name mediamtx --restart unless-stopped --network host \
|
||||||
|
--security-opt apparmor=unconfined \
|
||||||
|
-v /opt/stream-node/mediamtx.yml:/mediamtx.yml:ro \
|
||||||
|
-v /recordings:/recordings \
|
||||||
|
-v /slates:/slates:ro \
|
||||||
|
bluenviron/mediamtx:latest /mediamtx.yml
|
||||||
|
# smoke locale
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if curl -fsS http://127.0.0.1:9997/v3/paths/list >/dev/null; then
|
||||||
|
echo "mediamtx ready" | tee /opt/stream-node/ready
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "mediamtx NOT ready" | tee /opt/stream-node/ready
|
||||||
|
docker logs mediamtx || true
|
||||||
|
exit 1
|
||||||
|
|
||||||
runcmd:
|
runcmd:
|
||||||
- systemctl enable --now docker
|
- [bash, /opt/stream-node/bootstrap.sh]
|
||||||
- mkdir -p /opt/stream-node /recordings /slates
|
|
||||||
- |
|
|
||||||
cat >/opt/stream-node/docker-compose.yml <<'EOF'
|
|
||||||
services:
|
|
||||||
mediamtx:
|
|
||||||
image: bluenviron/mediamtx:latest
|
|
||||||
network_mode: host
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- ./mediamtx.yml:/mediamtx.yml:ro
|
|
||||||
- /recordings:/recordings
|
|
||||||
- /slates:/slates:ro
|
|
||||||
command: /mediamtx.yml
|
|
||||||
EOF
|
|
||||||
- |
|
|
||||||
cat >/opt/stream-node/mediamtx.yml <<'EOF'
|
|
||||||
logLevel: info
|
|
||||||
api: yes
|
|
||||||
apiAddress: :9997
|
|
||||||
rtmp: yes
|
|
||||||
rtmpAddress: :1935
|
|
||||||
hls: yes
|
|
||||||
hlsAddress: :8888
|
|
||||||
paths:
|
|
||||||
all_others:
|
|
||||||
EOF
|
|
||||||
- cd /opt/stream-node && docker compose up -d
|
|
||||||
|
|||||||
Reference in New Issue
Block a user