164 lines
5.9 KiB
Ruby
164 lines
5.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "excon"
|
|
class Hosting::HetznerApis
|
|
def initialize(hetzner_host)
|
|
@host = hetzner_host
|
|
end
|
|
|
|
def reimage(server_id, hetzner_ssh_key: Config.hetzner_ssh_key, dist: "Ubuntu 22.04.2 LTS base")
|
|
unless hetzner_ssh_key
|
|
raise "hetzner_ssh_key is not set"
|
|
end
|
|
|
|
key_data = hetzner_ssh_key.split(" ")[1]
|
|
decoded_data = Base64.decode64(key_data)
|
|
fingerprint = OpenSSL::Digest::MD5.new(decoded_data).hexdigest
|
|
formatted_fingerprint = fingerprint.scan(/../).join(":")
|
|
connection = Excon.new(@host.connection_string,
|
|
user: @host.user,
|
|
password: @host.password,
|
|
headers: {"Content-Type" => "application/x-www-form-urlencoded"})
|
|
connection.post(path: "/boot/#{server_id}/linux",
|
|
body: URI.encode_www_form(dist: dist, lang: "en", authorized_key: formatted_fingerprint),
|
|
expects: 200)
|
|
|
|
connection.post(path: "/reset/#{server_id}", body: "type=hw", expects: 200)
|
|
nil
|
|
end
|
|
|
|
def add_key(name, key)
|
|
connection = Excon.new(@host.connection_string,
|
|
user: @host.user,
|
|
password: @host.password,
|
|
headers: {"Content-Type" => "application/x-www-form-urlencoded"})
|
|
connection.post(path: "/key",
|
|
body: URI.encode_www_form(name: name, data: key),
|
|
expects: 201)
|
|
nil
|
|
end
|
|
|
|
def delete_key(key)
|
|
key_data = key.split(" ")[1]
|
|
decoded_data = Base64.decode64(key_data)
|
|
fingerprint = OpenSSL::Digest::MD5.new(decoded_data).hexdigest
|
|
|
|
connection = Excon.new(@host.connection_string,
|
|
user: @host.user,
|
|
password: @host.password,
|
|
headers: {"Content-Type" => "application/x-www-form-urlencoded"})
|
|
connection.delete(path: "/key/#{fingerprint}", expects: [200, 404])
|
|
|
|
nil
|
|
end
|
|
|
|
def get_main_ip4
|
|
connection = Excon.new(@host.connection_string,
|
|
user: @host.user,
|
|
password: @host.password,
|
|
headers: {"Content-Type" => "application/x-www-form-urlencoded"})
|
|
response = connection.get(path: "/server/#{@host.server_identifier}",
|
|
expects: 200)
|
|
|
|
response_hash = JSON.parse(response.body)
|
|
response_hash.dig("server", "server_ip")
|
|
end
|
|
|
|
# Fetches and processes the IPs, subnets, and failovers from the Hetzner API.
|
|
# It then calls `find_matching_ips` to retrieve IP addresses that match with
|
|
# the host's IP address. This whole thing is needed because Hetzner API is
|
|
# simply not good enough to do this in one call. Also the failover IP
|
|
# implementation depends on the host server and the IP continues to live under
|
|
# the original server. host even if the failover is performed. So we need to
|
|
# check the failover IP separately.
|
|
def pull_ips
|
|
connection = Excon.new(@host.connection_string, user: @host.user, password: @host.password)
|
|
response = connection.get(path: "/subnet", expects: 200)
|
|
json_arr_subnets = JSON.parse(response.body)
|
|
|
|
response = connection.get(path: "/ip", expects: 200)
|
|
json_arr_ips = JSON.parse(response.body)
|
|
|
|
response = connection.get(path: "/failover", expects: [200, 404])
|
|
json_arr_failover = (response.status == 404) ? [] : JSON.parse(response.body)
|
|
|
|
addresses_with_assignment = process_ips_subnets_failovers(json_arr_ips, json_arr_subnets, json_arr_failover)
|
|
find_matching_ips(addresses_with_assignment)
|
|
end
|
|
|
|
def process_ips_subnets_failovers(ips, subnets, failovers)
|
|
failovers_map = failovers.each_with_object({}) do |failover, map|
|
|
map[failover["failover"]["ip"]] = failover
|
|
end
|
|
|
|
{ips: process_items(ips, failovers_map), subnets: process_items(subnets, failovers_map)}
|
|
end
|
|
|
|
def process_items(items, failovers_map)
|
|
items.map do |item|
|
|
item_info = item[item.keys.first]
|
|
failover_info = failovers_map[item_info["ip"]]
|
|
|
|
item_info["failover_ip"] = !!failover_info
|
|
item_info["active_server_ip"] = failover_info ? failover_info["failover"]["active_server_ip"] : item_info["server_ip"]
|
|
|
|
item_info
|
|
end
|
|
end
|
|
|
|
IpInfo = Struct.new(:ip_address, :source_host_ip, :is_failover, keyword_init: true)
|
|
|
|
# Finds IP addresses that match with the host's IP address. An important
|
|
# detail about this function is that; Hetzner API returns the failover IPv6
|
|
# addresses with active_server_ip set to the host's IPv6 address. Here in the
|
|
# below, you will realize that we only check the sshable.host address which is
|
|
# the host's IPv4 address. Therefore, the additional IPv6 subnets will be
|
|
# filtered out. If in future, this needs to be fixed, we'll have to find a way
|
|
# to also add the IPv6 subnets.
|
|
def find_matching_ips(result)
|
|
host_address = @host.vm_host.sshable.host
|
|
(
|
|
# Aggregate single-ip addresses.
|
|
result[:ips].filter_map do |ip|
|
|
next unless ip["active_server_ip"] == host_address
|
|
IpInfo.new(
|
|
ip_address: "#{ip["ip"]}/32",
|
|
source_host_ip: ip["server_ip"],
|
|
is_failover: ip["failover_ip"]
|
|
)
|
|
end +
|
|
|
|
# Aggregate subnets (including IPv6 /64 blocks).
|
|
result[:subnets].filter_map do |subnet|
|
|
next unless subnet["active_server_ip"] == host_address
|
|
|
|
# Check if it is IPv6 or not by the existence of colon in the IP address
|
|
mask = subnet["ip"].include?(":") ? 64 : subnet.fetch("mask")
|
|
|
|
IpInfo.new(
|
|
ip_address: "#{subnet["ip"]}/#{mask}",
|
|
source_host_ip: subnet["server_ip"],
|
|
is_failover: subnet["failover_ip"]
|
|
)
|
|
end
|
|
)
|
|
end
|
|
|
|
def pull_dc(server_id)
|
|
connection = Excon.new(@host.connection_string, user: @host.user, password: @host.password)
|
|
response = connection.get(path: "/server/#{server_id}", expects: 200)
|
|
json_server = JSON.parse(response.body)
|
|
json_server.dig("server", "dc")
|
|
end
|
|
|
|
def set_server_name(server_id, name)
|
|
connection = Excon.new(@host.connection_string,
|
|
user: @host.user,
|
|
password: @host.password,
|
|
headers: {"Content-Type" => "application/x-www-form-urlencoded"})
|
|
connection.post(path: "/server/#{server_id}",
|
|
body: URI.encode_www_form(server_name: name),
|
|
expects: 200)
|
|
end
|
|
end
|