port = $port; $pve_hostname = $host; } } $this->pve_hostname = $pve_hostname; $this->pve_username = $pve_username; $this->pve_realm = $pve_realm; $this->pve_password = $pve_password; $this->print_debug = false; # Default this to null, so we can check later on if were logged in or not. $this->pve_login_ticket = null; $this->pve_login_ticket_timestamp = null; $this->pve_cluster_node_list = null; $this->constructor_success = true; } /** * FUNCTION constructor_success * Verify construct object * @return boolean $this->constructor_success */ public function constructor_success() { return $this->constructor_success; } /** * FUNCTION convert_postfields_array_to_string * Convert postfields to string * @param array $postfields_array * @return string $postfields_string */ private function convert_postfields_array_to_string($postfields_array) { $postfields_key_values = array(); foreach ($postfields_array as $field_key => $field_value) { $postfields_key_values[] = urlencode($field_key) . "=" . urlencode($field_value); } $postfields_string = implode("&", $postfields_key_values); return $postfields_string; } /** * FUNCTION set_debug * Sets if we should print() debug information throughout the process, * to assist in troubleshooting... * @param boolean $on_off * @return boolean */ public function set_debug($on_off) { if (is_bool($on_off)) { $this->print_debug = $on_off; return true; } else { return false; } } /** * FUNCTION login * Performs login to PVE Server using JSON API, and obtains Access Ticket. * @return boolean */ public function login() { if (!$this->constructor_success) { return false; } # Prepare login variables. $login_postfields = array(); $login_postfields['username'] = $this->pve_username; $login_postfields['password'] = $this->pve_password; $login_postfields['realm'] = $this->pve_realm; $login_postfields_string = $this->convert_postfields_array_to_string($login_postfields); # Perform login request. $prox_ch = curl_init(); curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->pve_hostname}:{$this->port}/api2/json/access/ticket"); curl_setopt($prox_ch, CURLOPT_POST, true); curl_setopt($prox_ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $login_postfields_string); curl_setopt($prox_ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($prox_ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($prox_ch, CURLOPT_CONNECTTIMEOUT, 30); $login_ticket = curl_exec($prox_ch); $err = curl_error($prox_ch); if ($login_ticket === false) { $err = $err ? $err : "Unable connect to Proxmox"; throw new ProxmoxApiException($err); } curl_close($prox_ch); unset($prox_ch); unset($login_postfields_string); $login_ticket_data = json_decode($login_ticket, true); if ($this->whmcsDebugMode) { if (function_exists('logModuleCall')) { logModuleCall( "Proxmox", "https://{$this->pve_hostname}:{$this->port}/api2/json/access/ticket", print_r($login_postfields, true), '', print_r($login_ticket_data, true) . "\n {$err}", array($this->pve_username, $this->pve_password) ); self::$moduleLogs[] = [ "proxmoxVPS", "https://{$this->pve_hostname}:{$this->port}/api2/json/access/ticket", print_r($login_postfields, true), '', print_r($login_ticket_data, true), [$this->pve_username, $this->pve_password] ]; } } unset($login_postfields); if ($login_ticket_data == null || $login_ticket_data['data'] == null) { # Login failed. # Just to be safe, set this to null again. $this->pve_login_ticket_timestamp = null; throw new ProxmoxApiException(sprintf("Login to Proxmox host '%s' port '%s' failed", $this->pve_hostname, $this->port),401); return false; } else { # Login success. $this->pve_login_ticket = $login_ticket_data['data']; # We store a UNIX timestamp of when the ticket was generated here, so we can identify when we need # a new one expiration wise later on... $this->pve_login_ticket_timestamp = time(); return true; } } /** * FUNCTION pve_check_login_ticket * Checks if the login ticket is valid still, returns false if not. * Method of checking is purely by age of ticket right now... * @return boolean */ protected function pve_check_login_ticket() { if($this->pve_realm=="PVEAPIToken"){ return true; } if ($this->pve_login_ticket == null) { # Just to be safe, set this to null again. $this->pve_login_ticket_timestamp = null; return false; } if ($this->pve_login_ticket_timestamp >= (time() + 7200)) { # Reset login ticket object values. $this->pve_login_ticket = null; $this->pve_login_ticket_timestamp = null; return false; } else { return true; } } /** * FUNCTION pve_action * This method is responsible for the general cURL requests to the JSON API, * and sits behind the abstraction layer methods get/put/post/delete etc. * @param string $action_path * @param string $http_method * @param array $put_post_parameters * @return array */ private function pve_action($action_path, $http_method, $put_post_parameters = null) { if (!$this->constructor_success) { return false; } # Check if we have a prefixed / on the path, if not add one. if (substr($action_path, 0, 1) != "/") { $action_path = "/" . $action_path; } if (!$this->pve_check_login_ticket()) { if ($this->print_debug === true) { print("Error - Not logged into Proxmox Host. No Login Access Ticket found or Ticket Expired.\n"); } return false; } # Prepare cURL resource. $prox_ch = curl_init(); if ($this->print_debug === true) { print("\nURL - https://{$this->pve_hostname}:{$this->port}/api2/json" . $action_path . "\n"); } #GET parameters $getparameters = null; if (!empty($put_post_parameters) && $http_method == "GET") { $getparameters = "?"; foreach ($put_post_parameters as $k => $v) { $getparameters .= urlencode($k) . "=" . urlencode($v) . "&"; } $getparameters = substr($getparameters, 0, -1); } $comand = explode("/", $action_path); $comand = end($comand); if ($comand == "rrd") { curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->pve_hostname}:{$this->port}/api2/png" . $action_path . $getparameters); } else { curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->pve_hostname}:{$this->port}/api2/json" . $action_path . $getparameters); } $put_post_http_headers = array(); if($this->pve_realm=="PVEAPIToken"){ $put_post_http_headers[] = sprintf("Authorization: PVEAPIToken=%s=%s", $this->pve_username, $this->pve_password); }else{ $put_post_http_headers[] = "CSRFPreventionToken: " . $this->pve_login_ticket['CSRFPreventionToken']; } curl_setopt($prox_ch, CURLOPT_HEADER, true); # Lets decide what type of action we are taking... switch ($http_method) { case "GET": if($this->pve_realm=="PVEAPIToken"){ curl_setopt($prox_ch, CURLOPT_HTTPHEADER, $put_post_http_headers); } # Nothing extra to do. break; case "PUT": curl_setopt($prox_ch, CURLOPT_CUSTOMREQUEST, "PUT"); # Set "POST" data. $action_postfields_string = $this->convert_postfields_array_to_string($put_post_parameters); curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $action_postfields_string); unset($action_postfields_string); # Add required HTTP headers. curl_setopt($prox_ch, CURLOPT_HTTPHEADER, $put_post_http_headers); break; case "POST": curl_setopt($prox_ch, CURLOPT_POST, true); # Set POST data. $action_postfields_string = $this->convert_postfields_array_to_string($put_post_parameters); curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $action_postfields_string); unset($action_postfields_string); # Add required HTTP headers. curl_setopt($prox_ch, CURLOPT_HTTPHEADER, $put_post_http_headers); break; case "DELETE": curl_setopt($prox_ch, CURLOPT_CUSTOMREQUEST, "DELETE"); # No "POST" data required, the delete destination is specified in the URL. # Add required HTTP headers. curl_setopt($prox_ch, CURLOPT_HTTPHEADER, $put_post_http_headers); break; default: if ($this->print_debug === true) { print("Error - Invalid HTTP Method specified.\n"); } return false; } curl_setopt($prox_ch, CURLOPT_CONNECTTIMEOUT, 30); curl_setopt($prox_ch, CURLOPT_RETURNTRANSFER, true); if($this->pve_realm!="PVEAPIToken"){ curl_setopt($prox_ch, CURLOPT_COOKIE, "PVEAuthCookie=" . $this->pve_login_ticket['ticket']); } curl_setopt($prox_ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($prox_ch, CURLOPT_SSL_VERIFYHOST, false); $action_response = curl_exec($prox_ch); if(!$action_response ){ $error = curl_error($prox_ch); $errorCode = curl_errno($prox_ch); } $this->httpCode = curl_getinfo($prox_ch, CURLINFO_HTTP_CODE); $this->raw_message = $action_response; curl_close($prox_ch); unset($prox_ch); // ModulesGarden if ($this->whmcsDebugMode) { if (function_exists('logModuleCall')) { logModuleCall( "proxmoxVPS", $action_path, $http_method . " https://{$this->pve_hostname}:{$this->port}/api2/json" . $action_path . "\n" . print_r($put_post_parameters, true), '', sprintf("HTTP %s %s",$this->httpCode, $action_response), array($this->pve_username, $this->pve_password) ); self::$moduleLogs[] = [ "proxmoxVPS", $action_path, $http_method . " https://{$this->pve_hostname}:{$this->port}/api2/json" . $action_path . "\n" . print_r($put_post_parameters, true), '', sprintf("HTTP %s %s",$this->httpCode, $action_response), [$this->pve_username, $this->pve_password] ]; } } if($error){ throw new ProxmoxApiException($error, $errorCode); } $response = explode("\r\n\r\n", $action_response, 2); $header_response = explode("\r\n", $response[0], 2); $body_response = $response[1]; $action_response_array = json_decode( $body_response , true); if ($comand == "rrd") { return $action_response; } # Parse response, confirm HTTP response code etc. if ($this->httpCode && in_array($this->httpCode,[403,500]) || $this->httpCode > 500 && empty( $action_response_array['data'])){ $header_response[0] = str_replace("HTTP/1.1 {$this->httpCode}", "",$header_response[0] ); $message = $header_response[0] ? ucfirst(trim($header_response[0])) : 'Wrong response from server'; throw new ProxmoxApiException($message, $this->httpCode); } if ($this->httpCode) { if ($this->httpCode < 400 ) { return $action_response_array['data']; } else { $errors = null; if (!empty($action_response_array['errors'])) { $errors = "("; foreach ($action_response_array['errors'] as $k => $v) { $errors .= " [$k] - $v"; } $errors .= " )"; } return array('errors' => array( $errors)); } } else { return array('errors' => array('Invalid HTTP Response - ' . print_r($action_response, true))); } if (!empty($action_response_array['data'])) { return $action_response_array['data']; } } /** * FUNCTION whmcsDebugMode * Turn WHMCS DEBUG on * @author Grzegorz Draganik - ModulesGarden * @param bool $turnon */ public function debug($turnon = true) { $this->whmcsDebugMode = $turnon; } /** * FUNCTION reload_node_list * Returns the list of node names as provided by /api2/json/nodes. * We need this for future get/post/put/delete calls. * ie. $this->get("nodes/XXX/status"); where XXX is one of the values from this return array. * @return boolean */ public function reload_node_list() { if (!$this->constructor_success) { return false; } $node_list = $this->pve_action("/nodes", "GET"); if (count($node_list) > 0) { $nodes_array = array(); foreach ($node_list as $node) { $nodes_array[] = $node['node']; } $this->pve_cluster_node_list = $nodes_array; return true; } else { if ($this->print_debug === true) { print("Error - Empty list of nodes returned in this cluster.\n"); } return false; } } /** * FUNCTION get_node_list * Geting node from proxmox server * @return array | boolean */ public function get_node_list() { # We run this if we haven't queried for cluster nodes as yet, and cache it in the object. if ($this->pve_cluster_node_list == null) { if ($this->reload_node_list() === false) { return false; } } return $this->pve_cluster_node_list; } /** * FUNCTION get * GET request * @param string $action_path * @param array $parameters * @return array | boolean */ public function get($action_path, $parameters = false) { if (!$this->constructor_success) { return false; } # We run this if we haven't queried for cluster nodes as yet, and cache it in the object. if ($this->pve_cluster_node_list == null) { if ($this->reload_node_list() === false) { return false; } } return $this->processRequest($this->pve_action($action_path, "GET", $parameters)); } /** * FUNCTION put * PUT request * @param string $action_path * @param array $parameters * @return boolean | array */ public function put($action_path, $parameters) { if (!$this->constructor_success) { return false; } # We run this if we haven't queried for cluster nodes as yet, and cache it in the object. if ($this->pve_cluster_node_list == null) { if ($this->reload_node_list() === false) { return false; } } return $this->processRequest($this->pve_action($action_path, "PUT", $parameters)); } /** * FUNCTION post * POST request * @param string $action_path * @param string $parameters * @return boolean | array */ public function post($action_path, $parameters = array()) { if (!$this->constructor_success) { return false; } # We run this if we haven't queried for cluster nodes as yet, and cache it in the object. if ($this->pve_cluster_node_list == null) { if ($this->reload_node_list() === false) { return false; } } return $this->processRequest($this->pve_action($action_path, "POST", $parameters)); } /** * FUNCTION delete * Delete request * @param string $action_path * @return boolean | array */ public function delete($action_path) { if (!$this->constructor_success) { return false; } # We run this if we haven't queried for cluster nodes as yet, and cache it in the object. if ($this->pve_cluster_node_list == null) { if ($this->reload_node_list() === false) { return false; } } return $this->processRequest($this->pve_action($action_path, "DELETE")); } public function isProxmox4() { if (empty($this->_version)) { $info = $this->get('/version'); $this->_version = $info ['version']; } return version_compare($this->_version, "4.0", '>='); } abstract function processRequest($response); # Logout not required, PVEAuthCookie tokens have a 2 hour lifetime. public static function beginTransaction(){ self::$moduleLogs=[]; } public static function commit(){ foreach (self::$moduleLogs as &$log){ logModuleCall( $log[0], $log[1], $log[2], $log[3], $log[4], $log[5] ); unset($log); } } public function getPveHostname(){ return $this->pve_hostname; } /** * @return mixed */ public function getTokenId() { return $this->tokenId; } /** * @param mixed $tokenId * @return AbstractApi */ public function setTokenId($tokenId) { $this->tokenId = $tokenId; return $this; } /** * @return mixed|string */ public function getPort() { return $this->port; } /** * @param mixed|string $port * @return AbstractApi */ public function setPort($port) { $this->port = $port; return $this; } }