first commit: added functionality for server listing, ssh key management and boot configuration management
This commit is contained in:
commit
8078234ada
2
.env
Normal file
2
.env
Normal file
@ -0,0 +1,2 @@
|
||||
HETZNER_USERNAME="#ws+JdQtGCdL"
|
||||
HETZNER_PASSWORD="Kds007kds!"
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
2040
Cargo.lock
generated
Normal file
2040
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "robot_hetzner_rhai"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
dotenv = "0.15.0"
|
||||
prettytable = "0.10.0"
|
||||
reqwest = { version = "0.12.22", features = ["json", "blocking"] }
|
||||
rhai = { version = "1.22.2", features = ["serde"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.46.1", features = ["full"] }
|
21
examples/boot_management.rhai
Normal file
21
examples/boot_management.rhai
Normal file
@ -0,0 +1,21 @@
|
||||
// Get the boot configuration for a server
|
||||
// Replace 1825193 with the server number you want to fetch
|
||||
let boot_config = hetzner.get_boot_configuration(1825193);
|
||||
print(boot_config);
|
||||
|
||||
// Get the rescue boot configuration for a server
|
||||
// Replace 1825193 with the server number you want to fetch
|
||||
let rescue_config = hetzner.get_rescue_boot_configuration(1825193);
|
||||
print(rescue_config);
|
||||
|
||||
// Enable rescue mode
|
||||
// Replace 1825193 with the server number you want to enable rescue mode on
|
||||
// Replace "linux" with the desired OS
|
||||
// Replace the fingerprint with your SSH key fingerprint
|
||||
//let enabled_rescue = hetzner.enable_rescue_mode(1825193, "linux", ["13:dc:a2:1e:a9:d2:1d:a9:39:f4:44:c5:f1:00:ec:c7"]);
|
||||
//print(enabled_rescue);
|
||||
|
||||
// Disable rescue mode
|
||||
// Replace 1825193 with the server number you want to disable rescue mode on
|
||||
//let disabled_rescue = hetzner.disable_rescue_mode(1825193);
|
||||
//print(disabled_rescue);
|
9
examples/server_management.rhai
Normal file
9
examples/server_management.rhai
Normal file
@ -0,0 +1,9 @@
|
||||
// Get all servers and print them in a table
|
||||
let servers = hetzner.get_servers();
|
||||
print(servers);
|
||||
print_servers_table(servers);
|
||||
|
||||
// Get a specific server and print its details
|
||||
// Replace 1825193 with the server number you want to fetch
|
||||
let server = hetzner.get_server(1825193);
|
||||
print_server_details(server);
|
22
examples/ssh_key_management.rhai
Normal file
22
examples/ssh_key_management.rhai
Normal file
@ -0,0 +1,22 @@
|
||||
// Get all SSH keys and print them in a table
|
||||
let keys = hetzner.get_ssh_keys();
|
||||
print_ssh_keys_table(keys);
|
||||
|
||||
// Get a specific SSH key
|
||||
// Replace "13:dc:a2:1e:a9:d2:1d:a9:39:f4:44:c5:f1:00:ec:c7" with the fingerprint of the key you want to fetch
|
||||
let key = hetzner.get_ssh_key("13:dc:a2:1e:a9:d2:1d:a9:39:f4:44:c5:f1:00:ec:c7");
|
||||
print(key);
|
||||
|
||||
// Add a new SSH key
|
||||
// Replace "my-new-key" with the desired name and "ssh-rsa ..." with your public key data
|
||||
let new_key = hetzner.add_ssh_key("my-new-key", "ssh-rsa ...");
|
||||
print(new_key);
|
||||
|
||||
// Update an SSH key's name
|
||||
// Replace "cb:8b:ef:a7:fe:04:87:3f:e5:55:cd:12:e3:e8:9f:99" with the fingerprint of the key you want to update
|
||||
let updated_key = hetzner.update_ssh_key_name("cb:8b:ef:a7:fe:04:87:3f:e5:55:cd:12:e3:e8:9f:99", "my-updated-key-name");
|
||||
print(updated_key);
|
||||
|
||||
// Delete an SSH key
|
||||
// Replace "cb:8b:ef:a7:fe:04:87:3f:e5:55:cd:12:e3:e8:9f:99" with the fingerprint of the key you want to delete
|
||||
hetzner.delete_ssh_key("cb:8b:ef:a7:fe:04:87:3f:e5:55:cd:12:e3:e8:9f:99");
|
12
src/api/error.rs
Normal file
12
src/api/error.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use crate::api::models::ApiError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AppError {
|
||||
#[error("Request failed: {0}")]
|
||||
RequestError(#[from] reqwest::Error),
|
||||
#[error("API error: {0:?}")]
|
||||
ApiError(ApiError),
|
||||
#[error("Deserialization failed: {0}\nResponse body: {1}")]
|
||||
DeserializationError(String, String),
|
||||
}
|
204
src/api/mod.rs
Normal file
204
src/api/mod.rs
Normal file
@ -0,0 +1,204 @@
|
||||
pub mod error;
|
||||
pub mod models;
|
||||
|
||||
use self::models::{
|
||||
Boot, BootWrapper, ErrorResponse, Rescue, RescueWrapper, Server, ServerWrapper, SshKey,
|
||||
SshKeyWrapper,
|
||||
};
|
||||
use crate::config::Config;
|
||||
use error::AppError;
|
||||
use reqwest::blocking::Client as HttpClient;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
http_client: HttpClient,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(config: Config) -> Self {
|
||||
Self {
|
||||
http_client: HttpClient::new(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_response<T>(&self, response: reqwest::blocking::Response) -> Result<T, AppError>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
match response.status() {
|
||||
StatusCode::OK => {
|
||||
// Read the body as text first, then try to deserialize from that
|
||||
let text = response.text().unwrap_or_else(|_| "<failed to read body>".to_string());
|
||||
let result = serde_json::from_str(&text);
|
||||
match result {
|
||||
Ok(val) => Ok(val),
|
||||
Err(e) => {
|
||||
let deser_err = format!("{:?}", e);
|
||||
Err(AppError::DeserializationError(deser_err, text))
|
||||
}
|
||||
}
|
||||
},
|
||||
_status => {
|
||||
let error_response: ErrorResponse = response.json()?;
|
||||
Err(AppError::ApiError(error_response.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_server(&self, server_number: i32) -> Result<Server, AppError> {
|
||||
let response = self
|
||||
.http_client
|
||||
.get(format!(
|
||||
"{}/server/{}",
|
||||
self.config.api_url, server_number
|
||||
))
|
||||
.basic_auth(&self.config.username, Some(&self.config.password))
|
||||
.send()?;
|
||||
|
||||
let server_wrapper: ServerWrapper = self.handle_response(response)?;
|
||||
Ok(server_wrapper.server)
|
||||
}
|
||||
|
||||
pub fn get_servers(&self) -> Result<Vec<Server>, AppError> {
|
||||
let response = self
|
||||
.http_client
|
||||
.get(format!("{}/server", self.config.api_url))
|
||||
.basic_auth(&self.config.username, Some(&self.config.password))
|
||||
.send()?;
|
||||
|
||||
let server_wrappers: Vec<ServerWrapper> = self.handle_response(response)?;
|
||||
let servers = server_wrappers.into_iter().map(|sw| sw.server).collect();
|
||||
Ok(servers)
|
||||
}
|
||||
pub fn get_ssh_keys(&self) -> Result<Vec<SshKey>, AppError> {
|
||||
let response = self
|
||||
.http_client
|
||||
.get(format!("{}/key", self.config.api_url))
|
||||
.basic_auth(&self.config.username, Some(&self.config.password))
|
||||
.send()?;
|
||||
|
||||
let ssh_key_wrappers: Vec<SshKeyWrapper> = self.handle_response(response)?;
|
||||
let ssh_keys = ssh_key_wrappers.into_iter().map(|sw| sw.key).collect();
|
||||
Ok(ssh_keys)
|
||||
}
|
||||
|
||||
pub fn get_ssh_key(&self, fingerprint: &str) -> Result<SshKey, AppError> {
|
||||
let response = self
|
||||
.http_client
|
||||
.get(format!("{}/key/{}", self.config.api_url, fingerprint))
|
||||
.basic_auth(&self.config.username, Some(&self.config.password))
|
||||
.send()?;
|
||||
|
||||
let ssh_key_wrapper: SshKeyWrapper = self.handle_response(response)?;
|
||||
Ok(ssh_key_wrapper.key)
|
||||
}
|
||||
|
||||
pub fn add_ssh_key(&self, name: &str, data: &str) -> Result<SshKey, AppError> {
|
||||
let params = [("name", name), ("data", data)];
|
||||
let response = self
|
||||
.http_client
|
||||
.post(format!("{}/key", self.config.api_url))
|
||||
.basic_auth(&self.config.username, Some(&self.config.password))
|
||||
.form(¶ms)
|
||||
.send()?;
|
||||
|
||||
let ssh_key_wrapper: SshKeyWrapper = self.handle_response(response)?;
|
||||
Ok(ssh_key_wrapper.key)
|
||||
}
|
||||
|
||||
pub fn update_ssh_key_name(
|
||||
&self,
|
||||
fingerprint: &str,
|
||||
name: &str,
|
||||
) -> Result<SshKey, AppError> {
|
||||
let params = [("name", name)];
|
||||
let response = self
|
||||
.http_client
|
||||
.post(format!("{}/key/{}", self.config.api_url, fingerprint))
|
||||
.basic_auth(&self.config.username, Some(&self.config.password))
|
||||
.form(¶ms)
|
||||
.send()?;
|
||||
|
||||
let ssh_key_wrapper: SshKeyWrapper = self.handle_response(response)?;
|
||||
Ok(ssh_key_wrapper.key)
|
||||
}
|
||||
|
||||
pub fn delete_ssh_key(&self, fingerprint: &str) -> Result<(), AppError> {
|
||||
self.http_client
|
||||
.delete(format!("{}/key/{}", self.config.api_url, fingerprint))
|
||||
.basic_auth(&self.config.username, Some(&self.config.password))
|
||||
.send()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_boot_configuration(&self, server_number: i32) -> Result<Boot, AppError> {
|
||||
let response = self
|
||||
.http_client
|
||||
.get(format!("{}/boot/{}", self.config.api_url, server_number))
|
||||
.basic_auth(&self.config.username, Some(&self.config.password))
|
||||
.send()?;
|
||||
|
||||
let boot_wrapper: BootWrapper = self.handle_response(response)?;
|
||||
Ok(boot_wrapper.boot)
|
||||
}
|
||||
|
||||
pub fn get_rescue_boot_configuration(
|
||||
&self,
|
||||
server_number: i32,
|
||||
) -> Result<Rescue, AppError> {
|
||||
let response = self
|
||||
.http_client
|
||||
.get(format!(
|
||||
"{}/boot/{}/rescue",
|
||||
self.config.api_url, server_number
|
||||
))
|
||||
.basic_auth(&self.config.username, Some(&self.config.password))
|
||||
.send()?;
|
||||
|
||||
let rescue_wrapper: RescueWrapper = self.handle_response(response)?;
|
||||
Ok(rescue_wrapper.rescue)
|
||||
}
|
||||
|
||||
pub fn enable_rescue_mode(
|
||||
&self,
|
||||
server_number: i32,
|
||||
os: &str,
|
||||
authorized_keys: Option<&[String]>,
|
||||
) -> Result<Rescue, AppError> {
|
||||
let mut params = vec![("os", os)];
|
||||
if let Some(keys) = authorized_keys {
|
||||
for key in keys {
|
||||
params.push(("authorized_key[]", key));
|
||||
}
|
||||
}
|
||||
let response = self
|
||||
.http_client
|
||||
.post(format!(
|
||||
"{}/boot/{}/rescue",
|
||||
self.config.api_url, server_number
|
||||
))
|
||||
.basic_auth(&self.config.username, Some(&self.config.password))
|
||||
.form(¶ms)
|
||||
.send()?;
|
||||
|
||||
let rescue_wrapper: RescueWrapper = self.handle_response(response)?;
|
||||
Ok(rescue_wrapper.rescue)
|
||||
}
|
||||
|
||||
pub fn disable_rescue_mode(&self, server_number: i32) -> Result<Rescue, AppError> {
|
||||
let response = self
|
||||
.http_client
|
||||
.delete(format!(
|
||||
"{}/boot/{}/rescue",
|
||||
self.config.api_url, server_number
|
||||
))
|
||||
.basic_auth(&self.config.username, Some(&self.config.password))
|
||||
.send()?;
|
||||
|
||||
let rescue_wrapper: RescueWrapper = self.handle_response(response)?;
|
||||
Ok(rescue_wrapper.rescue)
|
||||
}
|
||||
}
|
382
src/api/models.rs
Normal file
382
src/api/models.rs
Normal file
@ -0,0 +1,382 @@
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ServerWrapper {
|
||||
pub server: Server,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_rhai_type)]
|
||||
pub struct Server {
|
||||
pub server_ip: String,
|
||||
pub server_ipv6_net: String,
|
||||
pub server_number: i32,
|
||||
pub server_name: String,
|
||||
pub product: String,
|
||||
pub dc: String,
|
||||
pub traffic: String,
|
||||
pub status: String,
|
||||
pub cancelled: bool,
|
||||
pub paid_until: String,
|
||||
pub ip: Vec<String>,
|
||||
pub subnet: Option<Vec<Subnet>>,
|
||||
pub reset: Option<bool>,
|
||||
pub rescue: Option<bool>,
|
||||
pub vnc: Option<bool>,
|
||||
pub windows: Option<bool>,
|
||||
pub plesk: Option<bool>,
|
||||
pub cpanel: Option<bool>,
|
||||
pub wol: Option<bool>,
|
||||
pub hot_swap: Option<bool>,
|
||||
pub linked_storagebox: Option<i32>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Server")
|
||||
.with_get("server_ip", |s: &mut Server| s.server_ip.clone())
|
||||
.with_get("server_ipv6_net", |s: &mut Server| s.server_ipv6_net.clone())
|
||||
.with_get("server_number", |s: &mut Server| s.server_number)
|
||||
.with_get("server_name", |s: &mut Server| s.server_name.clone())
|
||||
.with_get("product", |s: &mut Server| s.product.clone())
|
||||
.with_get("dc", |s: &mut Server| s.dc.clone())
|
||||
.with_get("traffic", |s: &mut Server| s.traffic.clone())
|
||||
.with_get("status", |s: &mut Server| s.status.clone())
|
||||
.with_get("cancelled", |s: &mut Server| s.cancelled)
|
||||
.with_get("paid_until", |s: &mut Server| s.paid_until.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_rhai_type)]
|
||||
pub struct Subnet {
|
||||
pub ip: String,
|
||||
pub mask: String,
|
||||
}
|
||||
|
||||
impl Subnet {
|
||||
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Subnet")
|
||||
.with_get("ip", |s: &mut Subnet| s.ip.clone())
|
||||
.with_get("mask", |s: &mut Subnet| s.mask.clone());
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct SshKeyWrapper {
|
||||
pub key: SshKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_rhai_type)]
|
||||
pub struct SshKey {
|
||||
pub name: String,
|
||||
pub fingerprint: String,
|
||||
#[serde(rename = "type")]
|
||||
pub key_type: String,
|
||||
pub size: i32,
|
||||
pub data: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
impl SshKey {
|
||||
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("SshKey")
|
||||
.with_get("name", |s: &mut SshKey| s.name.clone())
|
||||
.with_get("fingerprint", |s: &mut SshKey| s.fingerprint.clone())
|
||||
.with_get("key_type", |s: &mut SshKey| s.key_type.clone())
|
||||
.with_get("size", |s: &mut SshKey| s.size)
|
||||
.with_get("data", |s: &mut SshKey| s.data.clone())
|
||||
.with_get("created_at", |s: &mut SshKey| s.created_at.clone());
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct BootWrapper {
|
||||
pub boot: Boot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_rhai_type)]
|
||||
pub struct Boot {
|
||||
pub rescue: Rescue,
|
||||
pub linux: Linux,
|
||||
pub vnc: Vnc,
|
||||
pub windows: Option<Windows>,
|
||||
pub plesk: Option<Plesk>,
|
||||
pub cpanel: Option<Cpanel>,
|
||||
}
|
||||
|
||||
impl Boot {
|
||||
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Boot")
|
||||
.with_get("rescue", |b: &mut Boot| b.rescue.clone())
|
||||
.with_get("linux", |b: &mut Boot| b.linux.clone())
|
||||
.with_get("vnc", |b: &mut Boot| b.vnc.clone())
|
||||
.with_get("windows", |b: &mut Boot| b.windows.clone())
|
||||
.with_get("plesk", |b: &mut Boot| b.plesk.clone())
|
||||
.with_get("cpanel", |b: &mut Boot| b.cpanel.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct RescueWrapper {
|
||||
pub rescue: Rescue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_rhai_type)]
|
||||
pub struct Rescue {
|
||||
pub server_ip: String,
|
||||
pub server_ipv6_net: String,
|
||||
pub server_number: i32,
|
||||
#[serde(deserialize_with = "string_or_seq_string")]
|
||||
pub os: Vec<String>,
|
||||
pub active: bool,
|
||||
pub password: Option<String>,
|
||||
pub authorized_key: Vec<String>,
|
||||
pub host_key: Vec<String>,
|
||||
}
|
||||
|
||||
impl Rescue {
|
||||
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Rescue")
|
||||
.with_get("server_ip", |r: &mut Rescue| r.server_ip.clone())
|
||||
.with_get("server_ipv6_net", |r: &mut Rescue| {
|
||||
r.server_ipv6_net.clone()
|
||||
})
|
||||
.with_get("server_number", |r: &mut Rescue| r.server_number)
|
||||
.with_get("os", |r: &mut Rescue| r.os.clone())
|
||||
.with_get("active", |r: &mut Rescue| r.active)
|
||||
.with_get("password", |r: &mut Rescue| r.password.clone())
|
||||
.with_get("authorized_key", |r: &mut Rescue| {
|
||||
r.authorized_key.clone()
|
||||
})
|
||||
.with_get("host_key", |r: &mut Rescue| r.host_key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_rhai_type)]
|
||||
pub struct Linux {
|
||||
pub server_ip: String,
|
||||
pub server_ipv6_net: String,
|
||||
pub server_number: i32,
|
||||
#[serde(deserialize_with = "string_or_seq_string")]
|
||||
pub dist: Vec<String>,
|
||||
#[serde(deserialize_with = "string_or_seq_string")]
|
||||
pub lang: Vec<String>,
|
||||
pub active: bool,
|
||||
pub password: Option<String>,
|
||||
pub authorized_key: Vec<String>,
|
||||
pub host_key: Vec<String>,
|
||||
}
|
||||
|
||||
impl Linux {
|
||||
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Linux")
|
||||
.with_get("server_ip", |l: &mut Linux| l.server_ip.clone())
|
||||
.with_get("server_ipv6_net", |l: &mut Linux| {
|
||||
l.server_ipv6_net.clone()
|
||||
})
|
||||
.with_get("server_number", |l: &mut Linux| l.server_number)
|
||||
.with_get("dist", |l: &mut Linux| l.dist.clone())
|
||||
.with_get("lang", |l: &mut Linux| l.lang.clone())
|
||||
.with_get("active", |l: &mut Linux| l.active)
|
||||
.with_get("password", |l: &mut Linux| l.password.clone())
|
||||
.with_get("authorized_key", |l: &mut Linux| {
|
||||
l.authorized_key.clone()
|
||||
})
|
||||
.with_get("host_key", |l: &mut Linux| l.host_key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_rhai_type)]
|
||||
pub struct Vnc {
|
||||
pub server_ip: String,
|
||||
pub server_ipv6_net: String,
|
||||
pub server_number: i32,
|
||||
#[serde(deserialize_with = "string_or_seq_string")]
|
||||
pub dist: Vec<String>,
|
||||
#[serde(deserialize_with = "string_or_seq_string")]
|
||||
pub lang: Vec<String>,
|
||||
pub active: bool,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl Vnc {
|
||||
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Vnc")
|
||||
.with_get("server_ip", |v: &mut Vnc| v.server_ip.clone())
|
||||
.with_get("server_ipv6_net", |v: &mut Vnc| v.server_ipv6_net.clone())
|
||||
.with_get("server_number", |v: &mut Vnc| v.server_number)
|
||||
.with_get("dist", |v: &mut Vnc| v.dist.clone())
|
||||
.with_get("lang", |v: &mut Vnc| v.lang.clone())
|
||||
.with_get("active", |v: &mut Vnc| v.active)
|
||||
.with_get("password", |v: &mut Vnc| v.password.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_rhai_type)]
|
||||
pub struct Windows {
|
||||
pub server_ip: String,
|
||||
pub server_ipv6_net: String,
|
||||
pub server_number: i32,
|
||||
#[serde(deserialize_with = "option_string_or_seq_string")]
|
||||
pub dist: Option<Vec<String>>,
|
||||
#[serde(deserialize_with = "option_string_or_seq_string")]
|
||||
pub lang: Option<Vec<String>>,
|
||||
pub active: bool,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl Windows {
|
||||
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Windows")
|
||||
.with_get("server_ip", |w: &mut Windows| w.server_ip.clone())
|
||||
.with_get("server_ipv6_net", |w: &mut Windows| {
|
||||
w.server_ipv6_net.clone()
|
||||
})
|
||||
.with_get("server_number", |w: &mut Windows| w.server_number)
|
||||
.with_get("dist", |w: &mut Windows| w.dist.clone())
|
||||
.with_get("lang", |w: &mut Windows| w.lang.clone())
|
||||
.with_get("active", |w: &mut Windows| w.active)
|
||||
.with_get("password", |w: &mut Windows| w.password.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_rhai_type)]
|
||||
pub struct Plesk {
|
||||
pub server_ip: String,
|
||||
pub server_ipv6_net: String,
|
||||
pub server_number: i32,
|
||||
#[serde(deserialize_with = "string_or_seq_string")]
|
||||
pub dist: Vec<String>,
|
||||
#[serde(deserialize_with = "string_or_seq_string")]
|
||||
pub lang: Vec<String>,
|
||||
pub active: bool,
|
||||
pub password: Option<String>,
|
||||
pub hostname: Option<String>,
|
||||
}
|
||||
|
||||
impl Plesk {
|
||||
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Plesk")
|
||||
.with_get("server_ip", |p: &mut Plesk| p.server_ip.clone())
|
||||
.with_get("server_ipv6_net", |p: &mut Plesk| {
|
||||
p.server_ipv6_net.clone()
|
||||
})
|
||||
.with_get("server_number", |p: &mut Plesk| p.server_number)
|
||||
.with_get("dist", |p: &mut Plesk| p.dist.clone())
|
||||
.with_get("lang", |p: &mut Plesk| p.lang.clone())
|
||||
.with_get("active", |p: &mut Plesk| p.active)
|
||||
.with_get("password", |p: &mut Plesk| p.password.clone())
|
||||
.with_get("hostname", |p: &mut Plesk| p.hostname.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_rhai_type)]
|
||||
pub struct Cpanel {
|
||||
pub server_ip: String,
|
||||
pub server_ipv6_net: String,
|
||||
pub server_number: i32,
|
||||
#[serde(deserialize_with = "string_or_seq_string")]
|
||||
pub dist: Vec<String>,
|
||||
#[serde(deserialize_with = "string_or_seq_string")]
|
||||
pub lang: Vec<String>,
|
||||
pub active: bool,
|
||||
pub password: Option<String>,
|
||||
pub hostname: Option<String>,
|
||||
}
|
||||
|
||||
impl Cpanel {
|
||||
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Cpanel")
|
||||
.with_get("server_ip", |c: &mut Cpanel| c.server_ip.clone())
|
||||
.with_get("server_ipv6_net", |c: &mut Cpanel| {
|
||||
c.server_ipv6_net.clone()
|
||||
})
|
||||
.with_get("server_number", |c: &mut Cpanel| c.server_number)
|
||||
.with_get("dist", |c: &mut Cpanel| c.dist.clone())
|
||||
.with_get("lang", |c: &mut Cpanel| c.lang.clone())
|
||||
.with_get("active", |c: &mut Cpanel| c.active)
|
||||
.with_get("password", |c: &mut Cpanel| c.password.clone())
|
||||
.with_get("hostname", |c: &mut Cpanel| c.hostname.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn string_or_seq_string<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
match value {
|
||||
Value::String(s) => Ok(vec![s]),
|
||||
Value::Array(a) => a
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
v.as_str()
|
||||
.map(ToString::to_string)
|
||||
.ok_or(serde::de::Error::custom("expected string"))
|
||||
})
|
||||
.collect(),
|
||||
_ => Err(serde::de::Error::custom(
|
||||
"expected string or array of strings",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn option_string_or_seq_string<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Vec<String>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
match value {
|
||||
Value::Null => Ok(None),
|
||||
Value::String(s) => Ok(Some(vec![s])),
|
||||
Value::Array(a) => Ok(Some(
|
||||
a.into_iter()
|
||||
.map(|v| {
|
||||
v.as_str()
|
||||
.map(ToString::to_string)
|
||||
.ok_or(serde::de::Error::custom("expected string"))
|
||||
})
|
||||
.collect::<Result<Vec<String>, _>>()?,
|
||||
)),
|
||||
_ => Err(serde::de::Error::custom(
|
||||
"expected string or array of strings",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ApiError {
|
||||
#[allow(dead_code)]
|
||||
pub status: i32,
|
||||
#[allow(dead_code)]
|
||||
pub code: String,
|
||||
#[allow(dead_code)]
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ErrorResponse {
|
||||
pub error: ApiError,
|
||||
}
|
25
src/config.rs
Normal file
25
src/config.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use std::env;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub api_url: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Result<Self, String> {
|
||||
let username = env::var("HETZNER_USERNAME")
|
||||
.map_err(|_| "HETZNER_USERNAME environment variable not set".to_string())?;
|
||||
let password = env::var("HETZNER_PASSWORD")
|
||||
.map_err(|_| "HETZNER_PASSWORD environment variable not set".to_string())?;
|
||||
let api_url = env::var("HETZNER_API_URL")
|
||||
.unwrap_or_else(|_| "https://robot-ws.your-server.de".to_string());
|
||||
|
||||
Ok(Config {
|
||||
username,
|
||||
password,
|
||||
api_url,
|
||||
})
|
||||
}
|
||||
}
|
44
src/main.rs
Normal file
44
src/main.rs
Normal file
@ -0,0 +1,44 @@
|
||||
mod api;
|
||||
mod config;
|
||||
mod scripting;
|
||||
|
||||
use crate::api::Client;
|
||||
use crate::config::Config;
|
||||
use crate::scripting::setup_engine;
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::process;
|
||||
|
||||
fn main() {
|
||||
dotenv().ok();
|
||||
|
||||
if let Err(e) = run() {
|
||||
eprintln!("Error: {}", e);
|
||||
let mut source = e.source();
|
||||
while let Some(s) = source {
|
||||
eprintln!("Caused by: {}", s);
|
||||
source = s.source();
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() != 2 {
|
||||
eprintln!("Usage: {} <path-to-script>", args[0]);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config = Config::from_env()?;
|
||||
let client = Client::new(config.clone());
|
||||
|
||||
let (engine, mut scope) = setup_engine(client);
|
||||
|
||||
let script = fs::read_to_string(&args[1])?;
|
||||
|
||||
engine.run_with_scope(&mut scope, &script)?;
|
||||
|
||||
Ok(())
|
||||
}
|
63
src/scripting/boot.rs
Normal file
63
src/scripting/boot.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use crate::api::{
|
||||
models::{Boot, Rescue},
|
||||
Client,
|
||||
};
|
||||
use rhai::{plugin::*, Engine};
|
||||
|
||||
pub fn register(engine: &mut Engine) {
|
||||
let boot_module = exported_module!(boot_api);
|
||||
engine.register_global_module(boot_module.into());
|
||||
}
|
||||
|
||||
#[export_module]
|
||||
pub mod boot_api {
|
||||
use super::*;
|
||||
use rhai::EvalAltResult;
|
||||
|
||||
#[rhai_fn(name = "get_boot_configuration", return_raw)]
|
||||
pub fn get_boot_configuration(
|
||||
client: &mut Client,
|
||||
server_number: i64,
|
||||
) -> Result<Boot, Box<EvalAltResult>> {
|
||||
client
|
||||
.get_boot_configuration(server_number as i32)
|
||||
.map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_rescue_boot_configuration", return_raw)]
|
||||
pub fn get_rescue_boot_configuration(
|
||||
client: &mut Client,
|
||||
server_number: i64,
|
||||
) -> Result<Rescue, Box<EvalAltResult>> {
|
||||
client
|
||||
.get_rescue_boot_configuration(server_number as i32)
|
||||
.map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "enable_rescue_mode", return_raw)]
|
||||
pub fn enable_rescue_mode(
|
||||
client: &mut Client,
|
||||
server_number: i64,
|
||||
os: &str,
|
||||
authorized_keys: rhai::Array,
|
||||
) -> Result<Rescue, Box<EvalAltResult>> {
|
||||
let keys: Vec<String> = authorized_keys
|
||||
.into_iter()
|
||||
.map(|k| k.into_string().unwrap())
|
||||
.collect();
|
||||
|
||||
client
|
||||
.enable_rescue_mode(server_number as i32, os, Some(&keys))
|
||||
.map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "disable_rescue_mode", return_raw)]
|
||||
pub fn disable_rescue_mode(
|
||||
client: &mut Client,
|
||||
server_number: i64,
|
||||
) -> Result<Rescue, Box<EvalAltResult>> {
|
||||
client
|
||||
.disable_rescue_mode(server_number as i32)
|
||||
.map_err(|e| e.to_string().into())
|
||||
}
|
||||
}
|
30
src/scripting/mod.rs
Normal file
30
src/scripting/mod.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use crate::api::Client;
|
||||
use crate::api::models::{Server, SshKey};
|
||||
use rhai::{Engine, Scope};
|
||||
|
||||
pub mod server;
|
||||
pub mod ssh_keys;
|
||||
pub mod boot;
|
||||
|
||||
pub fn setup_engine(client: Client) -> (Engine, Scope<'static>) {
|
||||
let mut engine = Engine::new();
|
||||
let mut scope = Scope::new();
|
||||
|
||||
engine.build_type::<Server>();
|
||||
engine.build_type::<SshKey>();
|
||||
engine.build_type::<crate::api::models::Boot>();
|
||||
engine.build_type::<crate::api::models::Rescue>();
|
||||
engine.build_type::<crate::api::models::Linux>();
|
||||
engine.build_type::<crate::api::models::Vnc>();
|
||||
engine.build_type::<crate::api::models::Windows>();
|
||||
engine.build_type::<crate::api::models::Plesk>();
|
||||
engine.build_type::<crate::api::models::Cpanel>();
|
||||
|
||||
server::register(&mut engine);
|
||||
ssh_keys::register(&mut engine);
|
||||
boot::register(&mut engine);
|
||||
|
||||
scope.push("hetzner", client);
|
||||
|
||||
(engine, scope)
|
||||
}
|
92
src/scripting/server.rs
Normal file
92
src/scripting/server.rs
Normal file
@ -0,0 +1,92 @@
|
||||
use crate::api::{models::Server, Client};
|
||||
use prettytable::{row, Table};
|
||||
use rhai::{plugin::*, Array, Dynamic};
|
||||
|
||||
pub fn register(engine: &mut Engine) {
|
||||
let server_module = exported_module!(server_api);
|
||||
engine.register_global_module(server_module.into());
|
||||
}
|
||||
|
||||
#[export_module]
|
||||
pub mod server_api {
|
||||
use super::*;
|
||||
use rhai::EvalAltResult;
|
||||
|
||||
#[rhai_fn(name = "get_server", return_raw)]
|
||||
pub fn get_server(
|
||||
client: &mut Client,
|
||||
server_number: i64,
|
||||
) -> Result<Server, Box<EvalAltResult>> {
|
||||
client
|
||||
.get_server(server_number as i32)
|
||||
.map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_servers", return_raw)]
|
||||
pub fn get_servers(client: &mut Client) -> Result<Array, Box<EvalAltResult>> {
|
||||
let servers = client
|
||||
.get_servers()
|
||||
.map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?;
|
||||
Ok(servers.into_iter().map(Dynamic::from).collect())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "print_servers_table")]
|
||||
pub fn print_servers_table(servers: Array) {
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![
|
||||
"Server Number",
|
||||
"Server Name",
|
||||
"Server IP",
|
||||
"Product",
|
||||
"Datacenter",
|
||||
"Status"
|
||||
]);
|
||||
|
||||
for server_dynamic in servers {
|
||||
let server: Server = server_dynamic
|
||||
.try_cast::<Server>()
|
||||
.expect("could not cast to server");
|
||||
table.add_row(row![
|
||||
server.server_number,
|
||||
server.server_name,
|
||||
server.server_ip,
|
||||
server.product,
|
||||
server.dc,
|
||||
server.status
|
||||
]);
|
||||
}
|
||||
|
||||
table.printstd();
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "print_server_details")]
|
||||
pub fn print_server_details(server: Server) {
|
||||
let mut table = Table::new();
|
||||
table.add_row(row!["Property", "Value"]);
|
||||
table.add_row(row!["Server Number", server.server_number]);
|
||||
table.add_row(row!["Server Name", server.server_name]);
|
||||
table.add_row(row!["Server IP", server.server_ip]);
|
||||
table.add_row(row!["IPv6 Network", server.server_ipv6_net]);
|
||||
table.add_row(row!["Product", server.product]);
|
||||
table.add_row(row!["Datacenter", server.dc]);
|
||||
table.add_row(row!["Traffic", server.traffic]);
|
||||
table.add_row(row!["Status", server.status]);
|
||||
table.add_row(row!["Cancelled", server.cancelled]);
|
||||
table.add_row(row!["Paid Until", server.paid_until]);
|
||||
table.add_row(row!["Reset", server.reset.unwrap_or(false)]);
|
||||
table.add_row(row!["Rescue", server.rescue.unwrap_or(false)]);
|
||||
table.add_row(row!["VNC", server.vnc.unwrap_or(false)]);
|
||||
table.add_row(row!["Windows", server.windows.unwrap_or(false)]);
|
||||
table.add_row(row!["Plesk", server.plesk.unwrap_or(false)]);
|
||||
table.add_row(row!["cPanel", server.cpanel.unwrap_or(false)]);
|
||||
table.add_row(row!["WOL", server.wol.unwrap_or(false)]);
|
||||
table.add_row(row!["Hot Swap", server.hot_swap.unwrap_or(false)]);
|
||||
table.add_row(row![
|
||||
"Linked Storagebox",
|
||||
server
|
||||
.linked_storagebox
|
||||
.map_or("N/A".to_string(), |id| id.to_string())
|
||||
]);
|
||||
table.printstd();
|
||||
}
|
||||
}
|
91
src/scripting/ssh_keys.rs
Normal file
91
src/scripting/ssh_keys.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use crate::api::{models::SshKey, Client};
|
||||
use prettytable::{row, Table};
|
||||
use rhai::{plugin::*, Array, Dynamic, Engine};
|
||||
|
||||
pub fn register(engine: &mut Engine) {
|
||||
let ssh_keys_module = exported_module!(ssh_keys_api);
|
||||
engine.register_global_module(ssh_keys_module.into());
|
||||
}
|
||||
|
||||
#[export_module]
|
||||
pub mod ssh_keys_api {
|
||||
use super::*;
|
||||
use rhai::EvalAltResult;
|
||||
|
||||
#[rhai_fn(name = "get_ssh_keys", return_raw)]
|
||||
pub fn get_ssh_keys(client: &mut Client) -> Result<Array, Box<EvalAltResult>> {
|
||||
let ssh_keys = client
|
||||
.get_ssh_keys()
|
||||
.map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?;
|
||||
Ok(ssh_keys.into_iter().map(Dynamic::from).collect())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "get_ssh_key", return_raw)]
|
||||
pub fn get_ssh_key(
|
||||
client: &mut Client,
|
||||
fingerprint: &str,
|
||||
) -> Result<SshKey, Box<EvalAltResult>> {
|
||||
client
|
||||
.get_ssh_key(fingerprint)
|
||||
.map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "add_ssh_key", return_raw)]
|
||||
pub fn add_ssh_key(
|
||||
client: &mut Client,
|
||||
name: &str,
|
||||
data: &str,
|
||||
) -> Result<SshKey, Box<EvalAltResult>> {
|
||||
client
|
||||
.add_ssh_key(name, data)
|
||||
.map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "update_ssh_key_name", return_raw)]
|
||||
pub fn update_ssh_key_name(
|
||||
client: &mut Client,
|
||||
fingerprint: &str,
|
||||
name: &str,
|
||||
) -> Result<SshKey, Box<EvalAltResult>> {
|
||||
client
|
||||
.update_ssh_key_name(fingerprint, name)
|
||||
.map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "delete_ssh_key", return_raw)]
|
||||
pub fn delete_ssh_key(
|
||||
client: &mut Client,
|
||||
fingerprint: &str,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
client
|
||||
.delete_ssh_key(fingerprint)
|
||||
.map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
#[rhai_fn(name = "print_ssh_keys_table")]
|
||||
pub fn print_ssh_keys_table(keys: Array) {
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![
|
||||
"Name",
|
||||
"Fingerprint",
|
||||
"Type",
|
||||
"Size",
|
||||
"Created At"
|
||||
]);
|
||||
|
||||
for key_dynamic in keys {
|
||||
let key: SshKey = key_dynamic
|
||||
.try_cast::<SshKey>()
|
||||
.expect("could not cast to SshKey");
|
||||
table.add_row(row![
|
||||
key.name,
|
||||
key.fingerprint,
|
||||
key.key_type,
|
||||
key.size,
|
||||
key.created_at
|
||||
]);
|
||||
}
|
||||
|
||||
table.printstd();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user