diff --git a/src/inc/defines/config.php b/src/inc/defines/config.php old mode 100644 new mode 100755 index b72d25672..b026dd11f --- a/src/inc/defines/config.php +++ b/src/inc/defines/config.php @@ -108,6 +108,12 @@ class DConfig { const NOTIFICATIONS_PROXY_PORT = "notificationsProxyPort"; const NOTIFICATIONS_PROXY_TYPE = "notificationsProxyType"; + // Section: VastAI + const VAST_AI_API_KEY = "vastAiApiKey"; + const VAST_IMAGE_URL = "vastImageUrl"; + const VAST_IMAGE_LOGIN = "vastImageLogin"; + const VAST_HASHTOPOLIS_BASE_URL = "vastHashtopolisBaseUrl"; + static function getConstants() { try { $oClass = new ReflectionClass(__CLASS__); @@ -272,6 +278,14 @@ public static function getConfigType($config) { return DConfigType::TICKBOX; case DConfig::HC_ERROR_IGNORE: return DConfigType::STRING_INPUT; + case DConfig::VAST_AI_API_KEY: + return DConfigType::STRING_INPUT; + case DConfig::VAST_IMAGE_URL: + return DConfigType::STRING_INPUT; + case DConfig::VAST_IMAGE_LOGIN: + return DConfigType::STRING_INPUT; + case DConfig::VAST_HASHTOPOLIS_BASE_URL: + return DConfigType::STRING_INPUT; } return DConfigType::STRING_INPUT; } @@ -406,6 +420,14 @@ public static function getConfigDescription($config) { return "Also send 'isComplete' for each task on the User API when listing all tasks (might affect performance)"; case DConfig::HC_ERROR_IGNORE: return "Ignore error messages from crackers which contain given strings (multiple values separated by comma)"; + case DConfig::VAST_AI_API_KEY: + return "Vast.ai API Key from vast.ai"; + case DConfig::VAST_IMAGE_URL: + return "Docker image url to be loaded by Vast.ai instances. Do not change this unless you know the image to be compatible"; + case DConfig::VAST_IMAGE_LOGIN: + return "Docker image login string. ex (without quotes): '-u bob -p 9d8df!fd89ufZ docker.io'. This is optional, but prevents the instances you rent from hitting the docker per-IP address rate limit. A hub.docker.com account is free to create here. The email/password option is recommended for ease of use."; + case DConfig::VAST_HASHTOPOLIS_BASE_URL: + return "The root URL of this hashtopolis server where the Vast.ai images should connect. Note: If using HTTPS, the certificate MUST be valid. The URL MUST also be accessible via the internet. ex: https://example.com:8443"; } return $config; } diff --git a/src/inc/vastAiUtils.php b/src/inc/vastAiUtils.php new file mode 100755 index 000000000..4706d2bcd --- /dev/null +++ b/src/inc/vastAiUtils.php @@ -0,0 +1,254 @@ +internalArray = $array; + } + + public function getUptime() { + if (! $this->getVal('start_date')) { + return null; + } + $start = (int) $this->getVal('start_date'); + $start = round($start, 0); + $current = time(); + $delta = $current - $start; + + $days = floor($delta / (60 * 60 * 24)); + $delta %= (60 * 60 * 24); + + $hours = floor($delta / (60 * 60)); + $delta %= (60 * 60); + + $minutes = floor($delta / 60); + $seconds = $delta % 60; + + $duration = ''; + + if ($days > 0) { + $duration .= $days . ' day' . ($days > 1 ? 's' : '') . ' '; + } + + $duration .= sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds); + + return $duration; + } + + public function getFloatValOld($key, $precision) { + return round(floatval($this->internalArray[$key]), $precision); + } + + public function getStartingVoucher() { + return $this->getVal('extra_env')[1][1]; + } + + public function getFloatval($key, $precision) { + $number = $this->getVal($key); + $retNumber = (float) number_format($number,0,'.',''); + for ($n = 0; $n <= 8; $n++) { + if ($retNumber === 0.0){ + $retNumber = (double) number_format($number,$n,'.',''); + } else { + return number_format($number,$n+(max($precision-2,0)),'.',''); + } + } + return "<0.00000000"; + } + + public function getFloatVal100($key, $precision) { + return round(floatval($this->internalArray[$key])*100, $precision); + } + + public function getVal($key) { + if (array_key_exists($key, $this->internalArray)) { + return $this->internalArray[$key]; + } else { + return null; + } + } +} + +function vastAiDestroyInstance($apiKey, $id) { + $url = 'https://console.vast.ai/api/v0/instances/' . $id . '/?api_key=' . $apiKey; + $headers = array('Accept: application/json'); + $options = array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_CUSTOMREQUEST => "DELETE" + ); + + $ch = curl_init($url); + $options[CURLOPT_HTTPHEADER] = $headers; + curl_setopt_array($ch, $options); + + $response = curl_exec($ch); + curl_close($ch); + + if($response === false) { + return 'vastaiAgentSearch curl error: ' . curl_error($ch); + } + + return json_decode($response, true); +} + +function makeVastaiVoucher($id){ + $sub = substr(md5(rand()),0,10); + $voucher = $randomSubStr = 'vast' . $sub . $id; + AgentUtils::createVoucher($voucher); + return $voucher; +} + +// retrieves the instances that the account is currently renting +function getVastAiAgents($apiKey) { + $url = 'https://console.vast.ai/api/v0/instances?api_key=' . $apiKey; + $headers = array('Accept: application/json'); + $options = array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + ); + + $ch = curl_init($url); + $options[CURLOPT_HTTPHEADER] = $headers; + curl_setopt_array($ch, $options); + + $response = curl_exec($ch); + curl_close($ch); + + if($response === false) { + return'vastaiAgentSearch curl error: ' . curl_error($ch); + } + + return json_decode($response, true); +} + +// search current GPUs, this does not require an api key +// an example query input value would be +// $query = '{"verified": {"eq": true}, "external": {"eq": false}, "rentable": {"eq": true}, "compute_cap": {"gt": "600"}, "disk_space": {"gt": "10000"}, "order": [["score", "desc"]], "type": "on-demand"}'; +function searchGpus($query) { + $url = 'https://console.vast.ai/api/v0/bundles?q=' . urlencode($query); + $headers = array('Accept: application/json'); + $options = array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + ); + + $ch = curl_init($url); + $options[CURLOPT_HTTPHEADER] = $headers; + curl_setopt_array($ch, $options); + + $response = curl_exec($ch); + curl_close($ch); + + if($response === false) { + return'vastaiAgentSearch curl error: ' . curl_error($ch); + } + + return json_decode($response, true); +} + +function rentMachine($apiKey, $id, $imageUrl, $price, $disk, $voucher, $baseUrl, $imageLogin) { + // API endpoint + $url = 'https://console.vast.ai/api/v0/asks/' . $id . '/?api_key=' . $apiKey; + if ( $imageLogin === ''){ + $imageLogin = null; + } + + // Data to be sent + $data = array( + "client_id" => "me", + "image" => $imageUrl, + "env" => array( + "TZ" => "UTC", + "VOUCHER" => $voucher, + "HCATURL" => $baseUrl + ), + "disk" => (int) $disk, + "label" => "instance-" . $id, + "extra" => null, + "image_login" => $imageLogin, + "onstart" => "/root/hcat/run.sh", + "runtype" => "ssh", + "python_utf8" => false, + "lang_utf8" => false, + "use_jupyter_lab" => false, + "jupyter_dir" => null + ); + + // Initialize cURL session + $ch = curl_init(); + + // Set cURL options + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Accept: application/json', + 'Content-Type: application/json' + )); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + + // Execute the cURL request + $response = curl_exec($ch); + // REMOVE THIS LINE, for debugging + curl_close($ch); + + // Check for any errors + if ($response === false) { + return 'cURL error: ' . curl_error($ch); + } + + // Close cURL session + return json_decode($response, true); +} + +function doesVoucherExist($voucher){ + $vouchers = Factory::getRegVoucherFactory()->filter([]); + foreach ($vouchers as $voucher){ + $voucherValue = $voucher->getVoucher(); + if (str_contains($voucherValue, "vast.ai") == false){ + continue; + } + $index = strpos($voucherValue, "-"); + $voucherInstanceId = substr($voucherValue, $index); + if ($voucherInstanceId === $intsanceId){ + return $voucherValue; + } + } + return null; +} + + +// given an instanceId it goes through all vouchers +// containing the substr vast.ai and returns the +// voucher that contains the instanceId +function getVoucherForInstance($instanceId){ + $vouchers = Factory::getRegVoucherFactory()->filter([]); + foreach ($vouchers as $voucher){ + $voucherValue = $voucher->getVoucher(); + if (str_contains($voucherValue, "vast.ai") == false){ + continue; + } + $index = strpos($voucherValue, "-"); + $voucherInstanceId = substr($voucherValue, $index); + if ($voucherInstanceId === $intsanceId){ + return $voucherValue; + } + } + return null; +} + +function get_or($arr,$key,$default){ + if (!$arr[$key]){ + return $default; + } + return $arr[$key]; +} + +?> diff --git a/src/install/hashtopolis.sql b/src/install/hashtopolis.sql old mode 100644 new mode 100755 index 41badf626..65c57834d --- a/src/install/hashtopolis.sql +++ b/src/install/hashtopolis.sql @@ -170,7 +170,11 @@ INSERT INTO `Config` (`configId`, `configSectionId`, `item`, `value`) VALUES (74, 4, 'agentUtilThreshold1', '90'), (75, 4, 'agentUtilThreshold2', '75'), (76, 3, 'uApiSendTaskIsComplete', '0'), - (77, 1, 'hcErrorIgnore', 'DeviceGetFanSpeed'); + (77, 1, 'hcErrorIgnore', 'DeviceGetFanSpeed'), + (78, 8, 'vastAiApiKey', ''), + (79, 8, 'vastImageUrl', 'deadjakk/hcat-vast-dev:latest'), + (80, 8, 'vastImageLogin', ''), + (81, 8, 'vastHashtopolisBaseUrl', ''); CREATE TABLE `ConfigSection` ( `configSectionId` INT(11) NOT NULL, @@ -184,7 +188,9 @@ INSERT INTO `ConfigSection` (`configSectionId`, `sectionName`) VALUES (4, 'UI'), (5, 'Server'), (6, 'Multicast'), - (7, 'Notifications'); + (7, 'Notifications'), + (8, 'VastAI') +; CREATE TABLE `CrackerBinary` ( `crackerBinaryId` INT(11) NOT NULL, diff --git a/src/templates/agents/vastai.template.html b/src/templates/agents/vastai.template.html new file mode 100755 index 000000000..97aef2744 --- /dev/null +++ b/src/templates/agents/vastai.template.html @@ -0,0 +1,82 @@ +{%TEMPLATE->struct/head%} +{%TEMPLATE->struct/menu%} +
Destroy | +ID | +Total Cost ($/hr) | +Status | +GPU(s) | +GPU RAM (MB) | +CPU Name | +CPU Threads | +CPU RAM (MB) | +Verification Status | +Inet Cost Upload ($/GB) | +Inet Cost Download ($/GB) | +Storage Cost ($/GB/Month) | +Interruptible | +|
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
+ + | + {{ELSE}} ++ {{ENDIF}} + | [[gpu.getVal('id')]] | +[[gpu.getFloatVal('dph_total',2)]] | +[[gpu.getVal('actual_status')]] | +[[gpu.getVal('gpu_name')]] x [[gpu.getVal('num_gpus')]] | +[[gpu.getVal('gpu_ram')]] | +[[gpu.getVal('cpu_name')]] | +[[gpu.getVal('cpu_cores')]] | +[[gpu.getVal('cpu_ram')]] | +[[gpu.getVal('verification')]] | +[[gpu.getFloatVal('inet_up_cost',2)]] | +[[gpu.getFloatVal('inet_down_cost',2)]] | +[[gpu.getVal('storage_cost')]] | +{{IF [[gpu.getVal('is_bid')]]}} True {{ELSE}} False {{ENDIF}} | +
Uptime: [[gpu.getUptime()]] ssh -p [[gpu.getVal('ssh_port')]] root@[[gpu.getVal('ssh_host')]] |
+ [[gpu.getVal('status_msg')]] | +
Total Cost ($/hr) | +[[gpu.getVal('dph_total')]] | +
Reliability | +[[gpu.getFloatVal100('reliability2',2)]] % | +
GPU(s) Name | +[[gpu.getVal('gpu_name')]] x [[gpu.getVal('num_gpus')]] | +
GPU RAM (MB) | +[[gpu.getVal('gpu_ram')]] | +
CPU Name | +[[gpu.getVal('cpu_name')]] | +
CPU Threads | +[[gpu.getVal('cpu_cores_effective')]] | +
CPU RAM (MB) | +[[gpu.getVal('cpu_ram')]] | +
Internet Upload Cost ($/GB) | +[[gpu.getFloatVal('inet_up_cost',2)]] | +
Internet Download Cost ($/GB) | +[[gpu.getFloatVal('inet_down_cost',2)]] | +
Storage Cost ($/GB/Month) | +[[gpu.getFloatVal('storage_cost',2)]] | +
Verification Status | +[[gpu.getVal('verification')]] | +
Rentable | +{{IF [[gpu.getVal('rentable')]]}} True {{ELSE}} False {{ENDIF}} | +
Interruptible | +{{IF [[gpu.getVal('is_bid')]]}} True {{ELSE}} False {{ENDIF}} | +