adding payments for VM contracts

This commit is contained in:
ghe0 2025-05-08 13:03:21 +03:00
parent 5c74962ac6
commit 7b7ffbee58
Signed by: ghe0
GPG Key ID: 451028EE56A0FBB4
11 changed files with 324 additions and 113 deletions

@ -1,91 +0,0 @@
DEFINE TABLE account SCHEMAFULL;
DEFINE FIELD balance ON TABLE account TYPE int;
DEFINE FIELD tmp_locked ON TABLE account TYPE int;
DEFINE FIELD escrow ON TABLE account TYPE int;
DEFINE FIELD email ON TABLE account TYPE string;
DEFINE FIELD hratls_pubkey ON TABLE account TYPE string;
DEFINE FIELD vm_nodes ON TABLE account TYPE array<record>;
DEFINE FIELD app_nodes ON TABLE account TYPE array<record>;
DEFINE TABLE package SCHEMAFULL;
DEFINE FIELD url ON TABLE package TYPE array<string>;
DEFINE TABLE kernel SCHEMAFULL;
DEFINE FIELD url ON TABLE kernel TYPE array<string>;
DEFINE TABLE dtrfs SCHEMAFULL;
DEFINE FIELD url ON TABLE dtrfs TYPE array<string>;
DEFINE FIELD kernel ON TABLE dtrfs TYPE record<kernel>;
DEFINE TABLE vm_node SCHEMAFULL;
DEFINE FIELD country ON TABLE vm_node TYPE string;
DEFINE FIELD region ON TABLE vm_node TYPE string;
DEFINE FIELD city ON TABLE vm_node TYPE string;
DEFINE FIELD ip ON TABLE vm_node TYPE string;
DEFINE FIELD avail_mem_mb ON TABLE vm_node TYPE int;
DEFINE FIELD avail_vcpus ON TABLE vm_node TYPE int;
DEFINE FIELD avail_storage_gbs ON TABLE vm_node TYPE int;
DEFINE FIELD avail_ipv4 ON TABLE vm_node TYPE int;
DEFINE FIELD avail_ipv6 ON TABLE vm_node TYPE int;
DEFINE FIELD avail_ports ON TABLE vm_node TYPE int;
DEFINE FIELD max_ports_per_vm ON TABLE vm_node TYPE int;
DEFINE FIELD price ON TABLE vm_node TYPE int;
DEFINE FIELD offline_minutes ON TABLE vm_node TYPE int;
DEFINE TABLE vm_contract TYPE RELATION FROM account TO vm_node SCHEMAFULL;
DEFINE FIELD state ON TABLE vm_contract TYPE string;
DEFINE FIELD hostname ON TABLE vm_contract TYPE string;
DEFINE FIELD mapped_ports ON TABLE vm_contract TYPE array<[int, int]>;
DEFINE FIELD public_ipv4 ON TABLE vm_contract TYPE string;
DEFINE FIELD public_ipv6 ON TABLE vm_contract TYPE string;
DEFINE FIELD disk_size_gb ON TABLE vm_contract TYPE int;
DEFINE FIELD vcpus ON TABLE vm_contract TYPE int;
DEFINE FIELD memory_mb ON TABLE vm_contract TYPE int;
DEFINE FIELD dtrfs ON TABLE vm_contract TYPE record<dtrfs>;
DEFINE FIELD created_at ON TABLE vm_contract TYPE datetime;
DEFINE FIELD updated_at ON TABLE vm_contract TYPE datetime;
DEFINE FIELD price_per_unit ON TABLE vm_contract TYPE int;
DEFINE FIELD locked_nano ON TABLE vm_contract TYPE int;
DEFINE FIELD collected_at ON TABLE vm_contract TYPE datetime;
DEFINE TABLE app_node SCHEMAFULL;
DEFINE FIELD country ON TABLE app_node TYPE string;
DEFINE FIELD region ON TABLE app_node TYPE string;
DEFINE FIELD city ON TABLE app_node TYPE string;
DEFINE FIELD ip ON TABLE app_node TYPE string;
DEFINE FIELD avail_mem_mb ON TABLE app_node TYPE int;
DEFINE FIELD avail_vcpus ON TABLE app_node TYPE int;
DEFINE FIELD avail_storage_gbs ON TABLE app_node TYPE int;
DEFINE FIELD avail_ports ON TABLE app_node TYPE int;
DEFINE FIELD max_ports_per_app ON TABLE app_node TYPE int;
DEFINE FIELD price ON TABLE app_node TYPE int;
DEFINE FIELD offline_minutes ON TABLE app_node TYPE int;
DEFINE TABLE app_contract TYPE RELATION FROM account TO app_node SCHEMAFULL;
DEFINE FIELD state ON TABLE app_contract TYPE string;
DEFINE FIELD app_name ON TABLE app_contract TYPE string;
DEFINE FIELD mapped_ports ON TABLE app_contract TYPE array<[int, int]>;
DEFINE FIELD host_ipv4 ON TABLE app_contract TYPE string;
DEFINE FIELD vcpus ON TABLE app_contract TYPE int;
DEFINE FIELD memory_mb ON TABLE app_contract TYPE int;
DEFINE FIELD disk_size_gb ON TABLE app_contract TYPE int;
DEFINE FIELD created_at ON TABLE app_contract TYPE datetime;
DEFINE FIELD updated_at ON TABLE app_contract TYPE datetime;
DEFINE FIELD price_per_unit ON TABLE app_contract TYPE int;
DEFINE FIELD locked_nano ON TABLE app_contract TYPE int;
DEFINE FIELD collected_at ON TABLE app_contract TYPE datetime;
DEFINE FIELD mr_enclave ON TABLE app_contract TYPE record<package>;
DEFINE TABLE ban TYPE RELATION FROM account TO account;
DEFINE FIELD created_at ON TABLE ban TYPE datetime;
DEFINE TABLE kick TYPE RELATION FROM account TO account;
DEFINE FIELD created_at ON TABLE kick TYPE datetime;
DEFINE FIELD reason ON TABLE kick TYPE string;
DEFINE FIELD contract ON TABLE kick TYPE record<vm_contract|app_contract>;
DEFINE TABLE report TYPE RELATION FROM account TO vm_node|app_node;
DEFINE FIELD created_at ON TABLE ban TYPE datetime;
DEFINE FIELD reason ON TABLE ban TYPE string;
DEFINE TABLE operator TYPE RELATION FROM account TO vm_node|app_node;

@ -192,8 +192,8 @@ impl Operator {
pub async fn inspect(db: &Surreal<Client>, account: &str) -> Result<Option<Self>, Error> {
let mut result = db
.query(format!(
"$vm_nodes = (select id from vm_node where operator = account:{account}).id;
$app_nodes = (select id from app_node where operator = account:{account}).id;
"LET $vm_nodes = (select id from vm_node where operator = account:{account}).id;
LET $app_nodes = (select id from app_node where operator = account:{account}).id;
select *,
id as account,
email,

@ -30,7 +30,27 @@ pub struct VmNode {
pub avail_ports: u32,
pub max_ports_per_vm: u32,
pub price: u64,
pub offline_minutes: u64,
pub connected_at: Datetime,
pub disconnected_at: Datetime,
}
impl VmNode {
pub async fn register(self, db: &Surreal<Client>) -> Result<(), Error> {
Account::get_or_create(db, &self.operator.key().to_string()).await?;
let _: Option<VmNode> = db.upsert(self.id.clone()).content(self).await?;
Ok(())
}
pub async fn set_online(db: &Surreal<Client>, vm_node_id: &str) -> Result<(), Error> {
db.query(format!("UPDATE {VM_NODE}:{vm_node_id} SET connected_at = time::now();")).await?;
Ok(())
}
pub async fn set_offline(db: &Surreal<Client>, vm_node_id: &str) -> Result<(), Error> {
db.query(format!("UPDATE {VM_NODE}:{vm_node_id} SET disconnected_at = time::now();"))
.await?;
Ok(())
}
}
#[derive(Serialize)]
@ -51,14 +71,6 @@ impl VmNodeResources {
}
}
impl VmNode {
pub async fn register(self, db: &Surreal<Client>) -> Result<(), Error> {
Account::get_or_create(db, &self.operator.key().to_string()).await?;
let _: Option<VmNode> = db.upsert(self.id.clone()).content(self).await?;
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VmNodeWithReports {
pub id: RecordId,
@ -75,13 +87,10 @@ pub struct VmNodeWithReports {
pub avail_ports: u32,
pub max_ports_per_vm: u32,
pub price: u64,
pub offline_minutes: u64,
pub reports: Vec<Report>,
}
impl VmNodeWithReports {
// TODO: find a more elegant way to do this than importing gRPC in the DB module
// https://en.wikipedia.org/wiki/Dependency_inversion_principle
pub async fn find_by_filters(
db: &Surreal<Client>,
filters: vm_proto::VmNodeFilters,
@ -115,7 +124,7 @@ impl VmNodeWithReports {
if !filters.ip.is_empty() {
query += &format!("&& ip = '{}' ", filters.ip);
}
query += ";";
query += " && connected_at > disconnected_at;";
let mut result = db.query(query).await?;
let vm_nodes: Vec<Self> = result.take(0)?;
Ok(vm_nodes)
@ -191,7 +200,46 @@ impl NewVmReq {
}
pub async fn submit(self, db: &Surreal<Client>) -> Result<(), Error> {
let _: Vec<Self> = db.insert(NEW_VM_REQ).relation(self).await?;
let locked_nano = self.locked_nano;
let account = self.admin.key().to_string();
let vm_id = self.id.key().to_string();
let vm_node = self.vm_node.key().to_string();
// TODO: check for possible injection and maybe use .bind()
let query = format!(
"
BEGIN TRANSACTION;
UPDATE account:{account} SET balance -= {locked_nano};
IF account:{account}.balance < 0 {{
THROW 'Insufficient funds.'
}};
UPDATE account:{account} SET tmp_locked += {locked_nano};
RELATE
account:{account}
->new_vm_req:{vm_id}
->vm_node:{vm_node}
CONTENT {{
created_at: time::now(), hostname: '{}', vcpus: {}, memory_mb: {}, disk_size_gb: {},
extra_ports: {}, public_ipv4: {:?}, public_ipv6: {:?},
dtrfs_url: '{}', dtrfs_sha: '{}', kernel_url: '{}', kernel_sha: '{}',
price_per_unit: {}, locked_nano: {locked_nano}, error: ''
}};
COMMIT TRANSACTION;
",
self.hostname,
self.vcpus,
self.memory_mb,
self.disk_size_gb,
format!("{:?}", self.extra_ports,),
self.public_ipv4,
self.public_ipv6,
self.dtrfs_url,
self.dtrfs_sha,
self.kernel_url,
self.kernel_sha,
self.price_per_unit
);
//let _: Vec<Self> = db.insert(NEW_VM_REQ).relation(self).await?;
db.query(query).await?;
Ok(())
}
}
@ -366,9 +414,12 @@ impl ActiveVm {
collected_at: new_vm_req.created_at,
};
let admin_account = active_vm.admin.key().to_string();
let locked_nano = active_vm.locked_nano;
let _: Vec<ActiveVm> = db.insert(()).relation(active_vm).await?;
NewVmReq::delete(db, id).await?;
db.query(format!("UPDATE {ACCOUNT}:{admin_account} SET tmp_locked -= {locked_nano};"))
.await?;
Ok(())
}
@ -750,7 +801,8 @@ impl From<&old_brain::BrainData> for Vec<VmNode> {
avail_ports: old_node.avail_ports,
max_ports_per_vm: old_node.max_ports_per_vm,
price: old_node.price,
offline_minutes: old_node.offline_minutes,
disconnected_at: Datetime::default(),
connected_at: Datetime::default(),
});
}
nodes

@ -53,7 +53,8 @@ impl BrainVmDaemon for VmDaemonServer {
avail_ipv6: 0,
avail_ports: 0,
max_ports_per_vm: 0,
offline_minutes: 0,
disconnected_at: surrealdb::sql::Datetime::default(),
connected_at: surrealdb::sql::Datetime::default(),
}
.register(&self.db)
.await?;
@ -83,6 +84,7 @@ impl BrainVmDaemon for VmDaemonServer {
&auth.signature,
)?;
info!("Daemon {} connected to receive brain messages", pubkey);
let _ = db::VmNode::set_online(&self.db, &pubkey).await;
let (tx, rx) = mpsc::channel(6);
{
@ -194,7 +196,8 @@ impl BrainVmDaemon for VmDaemonServer {
_ => {}
},
Err(e) => {
log::warn!("Daemon disconnected: {e:?}");
log::warn!("Daemon disconnected for {pubkey}: {e:?}");
let _ = db::VmNode::set_offline(&self.db, &pubkey).await;
}
}
}

2
surql/README.md Normal file

@ -0,0 +1,2 @@
This is actually SurrealQL (`.surql`), but the files have the `.sql`
extension to enable syntax coloring.

28
surql/cron.sql Normal file

@ -0,0 +1,28 @@
FOR $contract IN (select * from active_vm fetch out) {
LET $operator = (select * from $contract.out.operator);
LET $node_is_online = $contract.out.connected_at > $contract.out.disconnected_at;
LET $price_per_minute = fn::vm_price_per_minute($contract.id);
LET $amount_due = $price_per_minute;
IF $amount_due < $contract.locked_nano {
$amount_due = $contract.locked_nano;
};
LET $escrow_multiplier = 1;
IF $operator.escrow > 5000 {
$escrow_multiplier = 5;
};
IF $node_is_online {
UPDATE $contract.id SET locked_nano -= $amount_due;
UPDATE $operator.id SET locked_nano += $amount_due * $escrow_multiplier;
} ELSE {
LET $compensation = $amount_due * 5;
IF $compensation > $operator.escrow {
$compensation = $operator.escrow ;
};
UPDATE $operator.id SET escrow -= $compensation;
UPDATE $contract.in SET balance += $compensation;
};
IF $amount_due == $contract.locked_nano {
fn::delete_vm($contract.id);
};
};

26
surql/functions.sql Normal file

@ -0,0 +1,26 @@
DEFINE FUNCTION OVERWRITE fn::vm_price_per_minute(
$vm_id: record
) {
LET $vm = (select * from $vm_id)[0];
LET $ip_price = 0;
IF $vm.public_ipv4.len() > 0 {
$ip_price = 10
};
RETURN ($vm.vcpus * 10) + (($vm.memory_mb + 256) / 200) + ($vm.disk_size_gb / 10) + $ip_price;
};
DEFINE FUNCTION OVERWRITE fn::delete_vm(
$vm_id: record
) {
LET $vm = (select * from $vm_id)[0];
LET $account = $vm.in;
LET $deleted_vm = $vm.patch([{
'op': 'replace',
'path': 'id',
'value': type::record("deleted_vm:" + record::id($vm.id))
}]);
UPDATE $account SET balance += $vm.locked_nano;
INSERT RELATION INTO deleted_vm ( $deleted_vm );
DELETE $vm.id;
};

@ -18,7 +18,8 @@ DEFINE FIELD avail_ipv6 ON TABLE vm_node TYPE int;
DEFINE FIELD avail_ports ON TABLE vm_node TYPE int;
DEFINE FIELD max_ports_per_vm ON TABLE vm_node TYPE int;
DEFINE FIELD price ON TABLE vm_node TYPE int;
DEFINE FIELD offline_minutes ON TABLE vm_node TYPE int;
DEFINE FIELD connected_at ON TABLE vm_node TYPE datetime;
DEFINE FIELD disconnected_at ON TABLE vm_node TYPE datetime;
DEFINE TABLE new_vm_req TYPE RELATION FROM account TO vm_node SCHEMAFULL;
DEFINE FIELD hostname ON TABLE new_vm_req TYPE string;
@ -74,7 +75,7 @@ DEFINE FIELD memory_mb ON TABLE deleted_vm TYPE int;
DEFINE FIELD dtrfs_sha ON TABLE deleted_vm TYPE string;
DEFINE FIELD kernel_sha ON TABLE deleted_vm TYPE string;
DEFINE FIELD created_at ON TABLE deleted_vm TYPE datetime;
DEFINE FIELD deleted_at ON TABLE deleted_vm TYPE datetime;
DEFINE FIELD deleted_at ON TABLE deleted_vm TYPE datetime DEFAULT time::now();
DEFINE FIELD price_per_unit ON TABLE deleted_vm TYPE int;
DEFINE TABLE app_node SCHEMAFULL;

77
surql/testing/data.sql Normal file

@ -0,0 +1,77 @@
INSERT RELATION {
id: active_vm:vm1,
in: account:user1,
out: vm_node:online_node,
collected_at: time::now(),
created_at: time::now(),
disk_size_gb: 10,
dtrfs_sha: 'd207644ee60d54009b6ecdfb720e2ec251cde31774dd249fcc7435aca0377990',
hostname: 'vm1',
kernel_sha: 'e765e56166ef321b53399b9638584d1279821dbe3d46191c1f66bbaa075e7919',
locked_nano: 80000000,
mapped_ports: [
[
44551,
22
]
],
memory_mb: 5000,
price_per_unit: 20000,
public_ipv4: '',
public_ipv6: '',
vcpus: 4
};
INSERT {
id: vm_node:online_node,
avail_ipv4: 0,
avail_ipv6: 0,
avail_mem_mb: 25000,
avail_ports: 19999,
avail_storage_gbs: 700,
avail_vcpus: 27,
city: 'Pula',
connected_at: time::now(),
country: 'HR',
disconnected_at: time::now() - 1m,
ip: '184.107.169.199',
max_ports_per_vm: 5,
operator: account:operator1,
price: 18000,
region: 'Istria'
};
INSERT {
id: vm_node:offline_node,
avail_ipv4: 0,
avail_ipv6: 0,
avail_mem_mb: 25000,
avail_ports: 19999,
avail_storage_gbs: 700,
avail_vcpus: 27,
city: 'Pula',
connected_at: time::now() - 1m,
country: 'HR',
disconnected_at: time::now(),
ip: '184.107.200.100',
max_ports_per_vm: 5,
operator: account:operator1,
price: 18000,
region: 'Istria'
};
INSERT {
balance: 10000000000,
email: '',
escrow: 0,
id: account:user1,
tmp_locked: 0
};
INSERT {
balance: 10000000000,
email: '',
escrow: 0,
id: account:operator1,
tmp_locked: 0
};

27
surql/testing/prepare.sh Executable file

@ -0,0 +1,27 @@
#!/bin/bash
cd -- "$( dirname -- "${BASH_SOURCE[0]}" )"
import="docker run -i --rm --net=host \
--volume "$(pwd)/../:/opt/scripts/" \
surrealdb/surrealdb:latest import \
--endpoint http://127.0.0.1:8000 \
--username root --password root \
--namespace testing --database testbrain"
sql="docker run -i --rm --net=host \
surrealdb/surrealdb:latest sql \
--hide-welcome \
--endpoint http://127.0.0.1:8000 \
--username root --password root \
--namespace testing --database testbrain"
echo DELETING EXISTING DATA:
echo "REMOVE DATABASE testbrain;" | $sql
echo CREATING TABLES:
$import /opt/scripts/tables.sql
echo LOADING FUNCTIONS:
$import /opt/scripts/functions.sql
echo LOADING MOCK DATA:
$import /opt/scripts/testing/data.sql

@ -0,0 +1,86 @@
-- ------------------------------
-- OPTION
-- ------------------------------
OPTION IMPORT;
-- ------------------------------
-- TABLE: account
-- ------------------------------
DEFINE TABLE account TYPE NORMAL SCHEMAFULL PERMISSIONS NONE;
DEFINE FIELD balance ON account TYPE int DEFAULT 0 PERMISSIONS FULL;
DEFINE FIELD email ON account TYPE string DEFAULT '' PERMISSIONS FULL;
DEFINE FIELD escrow ON account TYPE int DEFAULT 0 PERMISSIONS FULL;
DEFINE FIELD tmp_locked ON account TYPE int DEFAULT 0 PERMISSIONS FULL;
-- ------------------------------
-- TABLE DATA: account
-- ------------------------------
INSERT [ { balance: 242800000, email: '', escrow: 0, id: account:x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK, tmp_locked: 338400000 } ];
-- ------------------------------
-- TABLE: active_vm
-- ------------------------------
DEFINE TABLE active_vm TYPE RELATION IN account OUT vm_node SCHEMAFULL PERMISSIONS NONE;
DEFINE FIELD collected_at ON active_vm TYPE datetime PERMISSIONS FULL;
DEFINE FIELD created_at ON active_vm TYPE datetime PERMISSIONS FULL;
DEFINE FIELD disk_size_gb ON active_vm TYPE int PERMISSIONS FULL;
DEFINE FIELD dtrfs_sha ON active_vm TYPE string PERMISSIONS FULL;
DEFINE FIELD hostname ON active_vm TYPE string PERMISSIONS FULL;
DEFINE FIELD in ON active_vm TYPE record<account> PERMISSIONS FULL;
DEFINE FIELD kernel_sha ON active_vm TYPE string PERMISSIONS FULL;
DEFINE FIELD locked_nano ON active_vm TYPE int PERMISSIONS FULL;
DEFINE FIELD mapped_ports ON active_vm TYPE array<[int, int]> PERMISSIONS FULL;
DEFINE FIELD mapped_ports[*] ON active_vm TYPE [int, int] PERMISSIONS FULL;
DEFINE FIELD memory_mb ON active_vm TYPE int PERMISSIONS FULL;
DEFINE FIELD out ON active_vm TYPE record<vm_node> PERMISSIONS FULL;
DEFINE FIELD price_per_unit ON active_vm TYPE int PERMISSIONS FULL;
DEFINE FIELD public_ipv4 ON active_vm TYPE string PERMISSIONS FULL;
DEFINE FIELD public_ipv6 ON active_vm TYPE string PERMISSIONS FULL;
DEFINE FIELD vcpus ON active_vm TYPE int PERMISSIONS FULL;
-- ------------------------------
-- TABLE DATA: active_vm
-- ------------------------------
INSERT RELATION [ { __: true, collected_at: d'2025-05-17T16:43:16.050142468Z', created_at: d'2025-05-17T16:43:16.050142468Z', disk_size_gb: 10, dtrfs_sha: 'd207644ee60d54009b6ecdfb720e2ec251cde31774dd249fcc7435aca0377990', hostname: 'futuristic-toaster', id: active_vm:0u6F9brHNRiWhyvWB6aRrz4ZFLBt7D7oa6sWvjpy, in: account:x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK, kernel_sha: 'e765e56166ef321b53399b9638584d1279821dbe3d46191c1f66bbaa075e7919', locked_nano: 80399531, mapped_ports: [[44551, 22]], memory_mb: 5000, out: vm_node:2Uf5pxhxKTUm6gRMnpbJHYDuyA6BWUfFsdmPyWfbMV1f, price_per_unit: 20000, public_ipv4: '', public_ipv6: '', vcpus: 4 } ];
-- ------------------------------
-- TABLE: vm_node
-- ------------------------------
DEFINE TABLE vm_node TYPE NORMAL SCHEMAFULL PERMISSIONS NONE;
DEFINE FIELD avail_ipv4 ON vm_node TYPE int PERMISSIONS FULL;
DEFINE FIELD avail_ipv6 ON vm_node TYPE int PERMISSIONS FULL;
DEFINE FIELD avail_mem_mb ON vm_node TYPE int PERMISSIONS FULL;
DEFINE FIELD avail_ports ON vm_node TYPE int PERMISSIONS FULL;
DEFINE FIELD avail_storage_gbs ON vm_node TYPE int PERMISSIONS FULL;
DEFINE FIELD avail_vcpus ON vm_node TYPE int PERMISSIONS FULL;
DEFINE FIELD city ON vm_node TYPE string PERMISSIONS FULL;
DEFINE FIELD connected_at ON vm_node TYPE datetime PERMISSIONS FULL;
DEFINE FIELD country ON vm_node TYPE string PERMISSIONS FULL;
DEFINE FIELD disconnected_at ON vm_node TYPE datetime PERMISSIONS FULL;
DEFINE FIELD ip ON vm_node TYPE string PERMISSIONS FULL;
DEFINE FIELD max_ports_per_vm ON vm_node TYPE int PERMISSIONS FULL;
DEFINE FIELD operator ON vm_node TYPE record<account> PERMISSIONS FULL;
DEFINE FIELD price ON vm_node TYPE int PERMISSIONS FULL;
DEFINE FIELD region ON vm_node TYPE string PERMISSIONS FULL;
-- ------------------------------
-- TABLE DATA: vm_node
-- ------------------------------
INSERT [ { avail_ipv4: 0, avail_ipv6: 0, avail_mem_mb: 25000, avail_ports: 19999, avail_storage_gbs: 700, avail_vcpus: 27, city: 'Montréal', connected_at: d'2025-05-17T12:10:16.573626182Z', country: 'CA', disconnected_at: d'2025-05-17T12:10:14.300498863Z', id: vm_node:2Uf5pxhxKTUm6gRMnpbJHYDuyA6BWUfFsdmPyWfbMV1f, ip: '184.107.169.199', max_ports_per_vm: 5, operator: account:x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK, price: 18000, region: 'Quebec' } ];