init
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
import config from "../config.ts";
|
||||
import axios from "axios";
|
||||
import { VMManager, type VM } from "./base.ts";
|
||||
import fs from "node:fs";
|
||||
import { redis } from "../utils/redis.ts";
|
||||
|
||||
const HETZNER_TOKEN = config.HETZNER_TOKEN;
|
||||
const sshKeys = config.HETZNER_SSH_KEYS.split(",").map(Number);
|
||||
|
||||
export class Hetzner extends VMManager {
|
||||
size = "cpx11"; // cpx11, cx22 (not available in US yet)
|
||||
largeSize = "cpx31"; // cpx21, cpx31, ccx13, ccx23, cx32
|
||||
minRetries = 5;
|
||||
reuseVMs = true;
|
||||
id = "Hetzner";
|
||||
gateway = config.HETZNER_GATEWAY;
|
||||
imageId = config.HETZNER_IMAGE;
|
||||
|
||||
private getRandomDatacenter() {
|
||||
// US
|
||||
let datacenters = ["ash"];
|
||||
if (this.region === "USW") {
|
||||
datacenters = ["hil"];
|
||||
} else if (this.region === "EU") {
|
||||
datacenters = ["nbg1", "fsn1", "hel1"];
|
||||
}
|
||||
return datacenters[Math.floor(Math.random() * datacenters.length)];
|
||||
}
|
||||
|
||||
startVM = async (name: string) => {
|
||||
const data = {
|
||||
name: name,
|
||||
server_type: this.isLarge ? this.largeSize : this.size,
|
||||
start_after_create: true,
|
||||
image: Number(this.imageId),
|
||||
ssh_keys: sshKeys,
|
||||
public_net: {
|
||||
enable_ipv4: true,
|
||||
enable_ipv6: false,
|
||||
},
|
||||
// networks: [
|
||||
// this.networks[Math.floor(Math.random() * this.networks.length)],
|
||||
// ],
|
||||
// user_data: `replace with vbrowser.sh startup script if we want to boot vbrowser on instance creation (won't trigger on rebuild/restart)`
|
||||
labels: {
|
||||
[this.getTag()]: "1",
|
||||
},
|
||||
location: this.getRandomDatacenter(),
|
||||
};
|
||||
const response = await axios({
|
||||
method: "POST",
|
||||
url: `https://api.hetzner.cloud/v1/servers`,
|
||||
headers: {
|
||||
Authorization: "Bearer " + HETZNER_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "test",
|
||||
},
|
||||
data,
|
||||
});
|
||||
const id = response.data.server.id;
|
||||
return id;
|
||||
};
|
||||
|
||||
terminateVM = async (id: string) => {
|
||||
await axios({
|
||||
method: "DELETE",
|
||||
url: `https://api.hetzner.cloud/v1/servers/${id}`,
|
||||
headers: {
|
||||
Authorization: "Bearer " + HETZNER_TOKEN,
|
||||
"User-Agent": "test",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
rebootVM = async (id: string) => {
|
||||
// Reboot the VM
|
||||
await axios({
|
||||
method: "POST",
|
||||
url: `https://api.hetzner.cloud/v1/servers/${id}/actions/reboot`,
|
||||
headers: {
|
||||
Authorization: "Bearer " + HETZNER_TOKEN,
|
||||
"User-Agent": "test",
|
||||
},
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
reimageVM = async (id: string) => {
|
||||
// Rebuild the VM
|
||||
await axios({
|
||||
method: "POST",
|
||||
url: `https://api.hetzner.cloud/v1/servers/${id}/actions/rebuild`,
|
||||
headers: {
|
||||
Authorization: "Bearer " + HETZNER_TOKEN,
|
||||
"User-Agent": "test",
|
||||
},
|
||||
data: {
|
||||
image: Number(this.imageId),
|
||||
},
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
getVM = async (id: string) => {
|
||||
const response: any = await axios({
|
||||
method: "GET",
|
||||
url: `https://api.hetzner.cloud/v1/servers/${id}`,
|
||||
headers: {
|
||||
Authorization: "Bearer " + HETZNER_TOKEN,
|
||||
"User-Agent": "test",
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
"[GETVM] %s: %s rate limit remaining",
|
||||
id,
|
||||
response?.headers["ratelimit-remaining"],
|
||||
);
|
||||
await redis?.set(
|
||||
"hetznerApiRemaining",
|
||||
response?.headers["ratelimit-remaining"],
|
||||
);
|
||||
const server = this.mapServerObject(response.data.server);
|
||||
return server;
|
||||
};
|
||||
|
||||
listVMs = async (filter: string) => {
|
||||
const limit = this.getLimitSize();
|
||||
const pageCount = Math.ceil((limit || 1) / 50);
|
||||
const pages = Array.from(Array(pageCount).keys()).map((i) => i + 1);
|
||||
const responses: any[] = await Promise.all(
|
||||
pages.map((page) =>
|
||||
axios({
|
||||
method: "GET",
|
||||
url: `https://api.hetzner.cloud/v1/servers`,
|
||||
headers: {
|
||||
Authorization: "Bearer " + HETZNER_TOKEN,
|
||||
"User-Agent": "test",
|
||||
},
|
||||
params: {
|
||||
sort: "id:asc",
|
||||
page,
|
||||
per_page: 50,
|
||||
label_selector: filter,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
const responsesMapped = responses.map((response) =>
|
||||
response.data.servers.map(this.mapServerObject),
|
||||
);
|
||||
return responsesMapped.flat();
|
||||
};
|
||||
|
||||
powerOn = async (id: string) => {
|
||||
// Poweron the server (usually not needed)
|
||||
try {
|
||||
await axios({
|
||||
method: "POST",
|
||||
url: `https://api.hetzner.cloud/v1/servers/${id}/actions/poweron`,
|
||||
headers: {
|
||||
Authorization: "Bearer " + HETZNER_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "test",
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("%s failed to poweron", id);
|
||||
}
|
||||
};
|
||||
|
||||
attachToNetwork = async (id: string) => {
|
||||
// // Attach server to network (usually not needed)
|
||||
// try {
|
||||
// const response: any = await axios({
|
||||
// method: 'GET',
|
||||
// url: `https://api.hetzner.cloud/v1/servers/${id}`,
|
||||
// headers: {
|
||||
// Authorization: 'Bearer ' + HETZNER_TOKEN,
|
||||
// },
|
||||
// });
|
||||
// if (response.data.server.private_net?.[0] == null) {
|
||||
// await axios({
|
||||
// method: 'POST',
|
||||
// url: `https://api.hetzner.cloud/v1/servers/${id}/actions/attach_to_network`,
|
||||
// headers: {
|
||||
// Authorization: 'Bearer ' + HETZNER_TOKEN,
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// data: {
|
||||
// network:
|
||||
// this.networks[Math.floor(Math.random() * this.networks.length)],
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// } catch (e: any) {
|
||||
// console.log('%s failed to attach to network', id);
|
||||
// console.log(e.response?.data);
|
||||
// }
|
||||
};
|
||||
|
||||
updateSnapshot = async () => {
|
||||
const response = await axios({
|
||||
method: "POST",
|
||||
url: `https://api.hetzner.cloud/v1/servers`,
|
||||
headers: {
|
||||
Authorization: "Bearer " + HETZNER_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "test",
|
||||
},
|
||||
data: {
|
||||
name: "vBrowserSnapshot",
|
||||
server_type: "cpx11",
|
||||
start_after_create: true,
|
||||
image: "docker-ce", // 15512617 for Ubuntu 20.04
|
||||
ssh_keys: sshKeys,
|
||||
user_data: fs
|
||||
.readFileSync(import.meta.dirname + "/../../dev/vbrowser.sh")
|
||||
.toString()
|
||||
.replace("{VBROWSER_ADMIN_KEY}", config.VBROWSER_ADMIN_KEY),
|
||||
location: this.getRandomDatacenter(),
|
||||
},
|
||||
});
|
||||
const id = response.data.server.id;
|
||||
await new Promise((resolve) => setTimeout(resolve, 4 * 60 * 1000));
|
||||
// Validate snapshot server was created successfully
|
||||
// const response3 = await axios(
|
||||
// 'http://' + response.data.server.public_net?.ipv4?.ip + ':5000'
|
||||
// );
|
||||
const response2 = await axios({
|
||||
method: "POST",
|
||||
url: `https://api.hetzner.cloud/v1/servers/${id}/actions/create_image`,
|
||||
headers: {
|
||||
Authorization: "Bearer " + HETZNER_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const imageId = response2.data.image.id;
|
||||
await this.terminateVM(id);
|
||||
return imageId;
|
||||
};
|
||||
|
||||
mapServerObject = (server: any): VM => {
|
||||
const public_ip = server.public_net?.ipv4?.ip;
|
||||
// const private_ip = server.private_net?.[0]?.ip;
|
||||
const ip = public_ip;
|
||||
// We can use either the public or private IP for communicating between gateway and VM
|
||||
// Only signaling traffic goes through here since the video is transmitted over WebRTC
|
||||
// The private IP requires the server and gateway to be on the same network and there is a limit to the number of servers allowed
|
||||
return {
|
||||
id: server.id?.toString(),
|
||||
// The gateway handles SSL termination and proxies to the private IP
|
||||
host: ip ? `${this.gateway}/?ip=${ip}` : "",
|
||||
provider: this.id,
|
||||
large: this.isLarge,
|
||||
region: this.region,
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user