diff --git a/README.md b/README.md index ef795bc..c7f4276 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,28 @@ -This class provides the building blocks for someone wanting to use PHP to talk to Proxmox 2.0. -Relatively simple piece of code, just provides a get/put/post/delete abstraction layer as methods -on top of Proxmox's REST API, while also handling the Login Ticket headers required for authentication. +# PVE2 API - PHP Client # -See http://pve.proxmox.com/wiki/Proxmox_VE_API for information about how this API works. -API spec available at http://pve.proxmox.com/pve2-api-doc/ +This class provides the building blocks for someone wanting to use PHP to talk +to Proxmox 2.0+. Relatively simple piece of code, just provides a +get/put/post/delete abstraction layer as methods on top of Proxmox's REST API, +while also handling the Login Ticket headers required for authentication. -## Requirements: ## +See for information about how this +API works. API spec available at + +## Requirements ## PHP 5 with cURL (including SSL) support. -## Usage: ## +## Usage ## Example - Return status array for each Proxmox Host in this cluster. +```php require("./pve2-api-php-client/pve2_api.class.php"); # You can try/catch exception handle the constructor here if you want. $pve2 = new PVE2_API("hostname", "username", "realm", "password"); # realm above can be pve, pam or any other realm available. - /* Optional - enable debugging. It print()'s any results currently */ - // $pve2->set_debug(true); - if ($pve2->login()) { foreach ($pve2->get_node_list() as $node_name) { print_r($pve2->get("/nodes/".$node_name."/status")); @@ -30,20 +31,18 @@ Example - Return status array for each Proxmox Host in this cluster. print("Login to Proxmox Host failed.\n"); exit; } +``` Example - Create a new OpenVZ Container on the first host in the cluster. +```php require("./pve2-api-php-client/pve2_api.class.php"); # You can try/catch exception handle the constructor here if you want. $pve2 = new PVE2_API("hostname", "username", "realm", "password"); # realm above can be pve, pam or any other realm available. - /* Optional - enable debugging. It print()'s any results currently */ - // $pve2->set_debug(true); - if ($pve2->login()) { - # Get first node name. $nodes = $pve2->get_node_list(); $first_node = $nodes[0]; @@ -69,20 +68,18 @@ Example - Create a new OpenVZ Container on the first host in the cluster. print("Login to Proxmox Host failed.\n"); exit; } +``` Example - Modify DNS settings on an existing container on the first host. +```php require("./pve2-api-php-client/pve2_api.class.php"); # You can try/catch exception handle the constructor here if you want. $pve2 = new PVE2_API("hostname", "username", "realm", "password"); # realm above can be pve, pam or any other realm available. - /* Optional - enable debugging. It print()'s any results currently */ - // $pve2->set_debug(true); - if ($pve2->login()) { - # Get first node name. $nodes = $pve2->get_node_list(); $first_node = $nodes[0]; @@ -98,18 +95,17 @@ Example - Modify DNS settings on an existing container on the first host. print("Login to Proxmox Host failed.\n"); exit; } +``` Example - Delete an existing container. +```php require("./pve2-api-php-client/pve2_api.class.php"); # You can try/catch exception handle the constructor here if you want. $pve2 = new PVE2_API("hostname", "username", "realm", "password"); # realm above can be pve, pam or any other realm available. - /* Optional - enable debugging. It print()'s any results currently */ - // $pve2->set_debug(true); - if ($pve2->login()) { # NOTE - replace XXXX with node short name, and YYYY with container ID. var_dump($pve2->delete("/nodes/XXXX/openvz/YYYY")); @@ -117,6 +113,7 @@ Example - Delete an existing container. print("Login to Proxmox Host failed.\n"); exit; } +``` Licensed under the MIT License. See LICENSE file. diff --git a/TODO b/TODO index 6615912..73ed558 100644 --- a/TODO +++ b/TODO @@ -1 +1,2 @@ -- add a new class extending PVE2_API that provides abstraction methods to common tasks, ie. create VM/container, change container settings, etc. +- add a new class extending PVE2_API that provides abstraction methods to +common tasks, ie. create VM/container, change container settings, etc. diff --git a/pve2_api.class.php b/pve2_api.class.php index be7369c..6412a83 100755 --- a/pve2_api.class.php +++ b/pve2_api.class.php @@ -6,119 +6,124 @@ Copyright (c) 2012-2014 Nathan Sullivan -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -class PVE2_Exception extends RuntimeException {} - -class PVE2_API { - protected $hostname; - protected $username; - protected $realm; - protected $password; - protected $port; - protected $verify_ssl; - - protected $login_ticket = null; - protected $login_ticket_timestamp = null; - protected $cluster_node_list = null; - - public function __construct ($hostname, $username, $realm, $password, $port = 8006, $verify_ssl = false) { - if (empty($hostname) || empty($username) || empty($realm) || empty($password) || empty($port)) { - throw new PVE2_Exception("Hostname/Username/Realm/Password/Port required for PVE2_API object constructor.", 1); - } - // Check hostname resolves. - if (gethostbyname($hostname) == $hostname && !filter_var($hostname, FILTER_VALIDATE_IP)) { - throw new PVE2_Exception("Cannot resolve {$hostname}.", 2); - } - // Check port is between 1 and 65535. - if (!is_int($port) || $port < 1 || $port > 65535) { - throw new PVE2_Exception("Port must be an integer between 1 and 65535.", 6); - } - // Check that verify_ssl is boolean. - if (!is_bool($verify_ssl)) { - throw new PVE2_Exception("verify_ssl must be boolean.", 7); - } - - $this->hostname = $hostname; - $this->username = $username; - $this->realm = $realm; - $this->password = $password; - $this->port = $port; - $this->verify_ssl = $verify_ssl; - } - - /* - * bool login () - * Performs login to PVE Server using JSON API, and obtains Access Ticket. - */ - public function login () { - // Prepare login variables. - $login_postfields = array(); - $login_postfields['username'] = $this->username; - $login_postfields['password'] = $this->password; - $login_postfields['realm'] = $this->realm; - - $login_postfields_string = http_build_query($login_postfields); - unset($login_postfields); - - // Perform login request. - $prox_ch = curl_init(); - curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->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, $this->verify_ssl); - - $login_ticket = curl_exec($prox_ch); - $login_request_info = curl_getinfo($prox_ch); - - curl_close($prox_ch); - unset($prox_ch); - unset($login_postfields_string); - - if (!$login_ticket) { - // SSL negotiation failed or connection timed out - $this->login_ticket_timestamp = null; - return false; - } +class PVE2_Exception extends RuntimeException +{ +} - $login_ticket_data = json_decode($login_ticket, true); - if ($login_ticket_data == null || $login_ticket_data['data'] == null) { - // Login failed. - // Just to be safe, set this to null again. - $this->login_ticket_timestamp = null; - if ($login_request_info['ssl_verify_result'] == 1) { - throw new PVE2_Exception("Invalid SSL cert on {$this->hostname} - check that the hostname is correct, and that it appears in the server certificate's SAN list. Alternatively set the verify_ssl flag to false if you are using internal self-signed certs (ensure you are aware of the security risks before doing so).", 4); - } - return false; - } else { - // Login success. - $this->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->login_ticket_timestamp = time(); - $this->reload_node_list(); - return true; - } - } +class PVE2_API +{ + protected $hostname; + protected $username; + protected $realm; + protected $password; + protected $port; + protected $verify_ssl; + + protected $login_ticket = null; + protected $login_ticket_timestamp = null; + protected $cluster_node_list = null; + + public function __construct($hostname, $username, $realm, $password, $port = 8006, $verify_ssl = false) + { + if (empty($hostname) || empty($username) || empty($realm) || empty($password) || empty($port)) { + throw new PVE2_Exception("Hostname/Username/Realm/Password/Port required for PVE2_API object constructor.", 1); + } + // Check hostname resolves. + if (gethostbyname($hostname) == $hostname && ! filter_var($hostname, FILTER_VALIDATE_IP)) { + throw new PVE2_Exception("Cannot resolve {$hostname}.", 2); + } + // Check port is between 1 and 65535. + if ( ! is_int($port) || $port < 1 || $port > 65535) { + throw new PVE2_Exception("Port must be an integer between 1 and 65535.", 6); + } + // Check that verify_ssl is boolean. + if ( ! is_bool($verify_ssl)) { + throw new PVE2_Exception("verify_ssl must be boolean.", 7); + } + + $this->hostname = $hostname; + $this->username = $username; + $this->realm = $realm; + $this->password = $password; + $this->port = $port; + $this->verify_ssl = $verify_ssl; + } + + /* + * bool login () + * Performs login to PVE Server using JSON API, and obtains Access Ticket. + */ + public function login() + { + // Prepare login variables. + $login_postfields = []; + $login_postfields['username'] = $this->username; + $login_postfields['password'] = $this->password; + $login_postfields['realm'] = $this->realm; + + $login_postfields_string = http_build_query($login_postfields); + unset($login_postfields); + + // Perform login request. + $prox_ch = curl_init(); + curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->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, $this->verify_ssl); + + $login_ticket = curl_exec($prox_ch); + $login_request_info = curl_getinfo($prox_ch); + + curl_close($prox_ch); + unset($prox_ch); + unset($login_postfields_string); + + if ( ! $login_ticket) { + // SSL negotiation failed or connection timed out + $this->login_ticket_timestamp = null; + return false; + } + + $login_ticket_data = json_decode($login_ticket, true); + if ($login_ticket_data == null || $login_ticket_data['data'] == null) { + // Login failed. + // Just to be safe, set this to null again. + $this->login_ticket_timestamp = null; + if ($login_request_info['ssl_verify_result'] == 1) { + throw new PVE2_Exception("Invalid SSL cert on {$this->hostname} - check that the hostname is correct, and that it appears in the server certificate's SAN list. Alternatively set the verify_ssl flag to false if you are using internal self-signed certs (ensure you are aware of the security risks before doing so).", 4); + } + return false; + } else { + // Login success. + $this->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->login_ticket_timestamp = time(); + $this->reload_node_list(); + return true; + } + } # Sets the PVEAuthCookie # Attetion, after using this the user is logged into the web interface aswell! @@ -131,244 +136,252 @@ public function setCookie() { setrawcookie("PVEAuthCookie", $this->login_ticket['ticket'], 0, "/"); } - /* - * bool 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... - */ - protected function check_login_ticket () { - if ($this->login_ticket == null) { - // Just to be safe, set this to null again. - $this->login_ticket_timestamp = null; - return false; - } - if ($this->login_ticket_timestamp >= (time() + 7200)) { - // Reset login ticket object values. - $this->login_ticket = null; - $this->login_ticket_timestamp = null; - return false; - } else { - return true; - } - } - - /* - * object action (string action_path, string http_method[, array put_post_parameters]) - * 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. - */ - private function action ($action_path, $http_method, $put_post_parameters = null) { - // 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->check_login_ticket()) { - throw new PVE2_Exception("Not logged into Proxmox host. No Login access ticket found or ticket expired.", 3); - } - - // Prepare cURL resource. - $prox_ch = curl_init(); - - $put_post_http_headers = array(); - $put_post_http_headers[] = "CSRFPreventionToken: {$this->login_ticket['CSRFPreventionToken']}"; - // Lets decide what type of action we are taking... - switch ($http_method) { - case "GET": - // cURL used to translate POSTFIELDS into the query string when the - // request method was GET, but that doesn't seem to be the case any - // longer, so we need to build them into the query string ourselves. - $action_postfields_string = http_build_query($put_post_parameters); - if ((strpos($action_path, '?') === FALSE) { - $action_path .= '?' . $action_postfields_string; - } else { - $action_path .= '&' . $action_postfields_string; - } - unset($action_postfields_string); - break; - case "PUT": - // Set "POST" data. - $action_postfields_string = http_build_query($put_post_parameters); - curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $action_postfields_string); - unset($action_postfields_string); - - curl_setopt($prox_ch, CURLOPT_CUSTOMREQUEST, "PUT"); - - // 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 = http_build_query($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": - // No "POST" data required, the delete destination is specified in the URL. - curl_setopt($prox_ch, CURLOPT_CUSTOMREQUEST, "DELETE"); - - // Add required HTTP headers. - curl_setopt($prox_ch, CURLOPT_HTTPHEADER, $put_post_http_headers); - break; - default: - throw new PVE2_Exception("Error - Invalid HTTP Method specified.", 5); - return false; - } - - curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->hostname}:{$this->port}/api2/json{$action_path}"); - curl_setopt($prox_ch, CURLOPT_HEADER, true); - curl_setopt($prox_ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($prox_ch, CURLOPT_COOKIE, "PVEAuthCookie=".$this->login_ticket['ticket']); - curl_setopt($prox_ch, CURLOPT_SSL_VERIFYPEER, false); - - $action_response = curl_exec($prox_ch); - - curl_close($prox_ch); - unset($prox_ch); - - $split_action_response = explode("\r\n\r\n", $action_response, 2); - $header_response = $split_action_response[0]; - $body_response = $split_action_response[1]; - $action_response_array = json_decode($body_response, true); - - $action_response_export = var_export($action_response_array, true); - error_log("----------------------------------------------\n" . - "FULL RESPONSE:\n\n{$action_response}\n\nEND FULL RESPONSE\n\n" . - "Headers:\n\n{$header_response}\n\nEnd Headers\n\n" . - "Data:\n\n{$body_response}\n\nEnd Data\n\n" . - "RESPONSE ARRAY:\n\n{$action_response_export}\n\nEND RESPONSE ARRAY\n" . - "----------------------------------------------"); - - unset($action_response); - unset($action_response_export); - - // Parse response, confirm HTTP response code etc. - $split_headers = explode("\r\n", $header_response); - if (substr($split_headers[0], 0, 9) == "HTTP/1.1 ") { - $split_http_response_line = explode(" ", $split_headers[0]); - if ($split_http_response_line[1] == "200") { - if ($http_method == "PUT") { - return true; - } else { - return $action_response_array['data']; - } - } else { - error_log("This API Request Failed.\n" . - "HTTP Response - {$split_http_response_line[1]}\n" . - "HTTP Error - {$split_headers[0]}"); - return false; - } - } else { - error_log("Error - Invalid HTTP Response.\n" . var_export($split_headers, true)); - return false; - } - - if (!empty($action_response_array['data'])) { - return $action_response_array['data']; - } else { - error_log("\$action_response_array['data'] is empty. Returning false.\n" . - var_export($action_response_array['data'], true)); - return false; - } - } - - /* - * array 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. - */ - public function reload_node_list () { - $node_list = $this->get("/nodes"); - if (count($node_list) > 0) { - $nodes_array = array(); - foreach ($node_list as $node) { - $nodes_array[] = $node['node']; - } - $this->cluster_node_list = $nodes_array; - return true; - } else { - error_log(" Empty list of nodes returned in this cluster."); - return false; - } - } - - /* - * array get_node_list () - * - */ - 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->cluster_node_list == null) { - if ($this->reload_node_list() === false) { - return false; - } - } - - return $this->cluster_node_list; - } - - /* - * bool|int get_next_vmid () - * Get Last VMID from a Cluster or a Node - * returns a VMID, or false if not found. - */ - public function get_next_vmid () { - $vmid = $this->get("/cluster/nextid"); - if ($vmid == null) { - return false; - } else { - return $vmid; - } - } - - /* - * bool|string get_version () - * Return the version and minor revision of Proxmox Server - */ - public function get_version () { - $version = $this->get("/version"); - if ($version == null) { - return false; - } else { - return $version['version']; - } - } - - /* - * object/array? get (string action_path) - */ - public function get ($action_path, $parameters = array()) { - return $this->action($action_path, "GET", $parameters); - } - - /* - * bool put (string action_path, array parameters) - */ - public function put ($action_path, $parameters = array()) { - return $this->action($action_path, "PUT", $parameters); - } - - /* - * bool post (string action_path, array parameters) - */ - public function post ($action_path, $parameters = array()) { - return $this->action($action_path, "POST", $parameters); - } - - /* - * bool delete (string action_path) - */ - public function delete ($action_path) { - return $this->action($action_path, "DELETE"); - } - - // Logout not required, PVEAuthCookie tokens have a 2 hour lifetime. + /* + * bool 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... + */ + protected function check_login_ticket() + { + if ($this->login_ticket == null) { + // Just to be safe, set this to null again. + $this->login_ticket_timestamp = null; + return false; + } + if ($this->login_ticket_timestamp >= (time() + 7200)) { + // Reset login ticket object values. + $this->login_ticket = null; + $this->login_ticket_timestamp = null; + return false; + } else { + return true; + } + } + + /* + * object action (string action_path, string http_method[, array put_post_parameters]) + * 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. + */ + private function action($action_path, $http_method, $put_post_parameters = null) + { + // 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->check_login_ticket()) { + throw new PVE2_Exception("Not logged into Proxmox host. No Login access ticket found or ticket expired.", 3); + } + + // Prepare cURL resource. + $prox_ch = curl_init(); + + $put_post_http_headers = []; + $put_post_http_headers[] = "CSRFPreventionToken: {$this->login_ticket['CSRFPreventionToken']}"; + // Lets decide what type of action we are taking... + switch ($http_method) { + case "GET": + // cURL used to translate POSTFIELDS into the query string when the + // request method was GET, but that doesn't seem to be the case any + // longer, so we need to build them into the query string ourselves. + $action_postfields_string = http_build_query($put_post_parameters); + if (strpos($action_path, '?') === false) { + $action_path .= '?' . $action_postfields_string; + } else { + $action_path .= '&' . $action_postfields_string; + } + unset($action_postfields_string); + break; + case "PUT": + // Set "POST" data. + $action_postfields_string = http_build_query($put_post_parameters); + curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $action_postfields_string); + unset($action_postfields_string); + + curl_setopt($prox_ch, CURLOPT_CUSTOMREQUEST, "PUT"); + + // 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 = http_build_query($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": + // No "POST" data required, the delete destination is specified in the URL. + curl_setopt($prox_ch, CURLOPT_CUSTOMREQUEST, "DELETE"); + + // Add required HTTP headers. + curl_setopt($prox_ch, CURLOPT_HTTPHEADER, $put_post_http_headers); + break; + default: + throw new PVE2_Exception("Error - Invalid HTTP Method specified.", 5); + return false; + } + + curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->hostname}:{$this->port}/api2/json{$action_path}"); + curl_setopt($prox_ch, CURLOPT_HEADER, true); + curl_setopt($prox_ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($prox_ch, CURLOPT_COOKIE, "PVEAuthCookie=" . $this->login_ticket['ticket']); + curl_setopt($prox_ch, CURLOPT_SSL_VERIFYPEER, false); + + $action_response = curl_exec($prox_ch); + + curl_close($prox_ch); + unset($prox_ch); + + $split_action_response = explode("\r\n\r\n", $action_response, 2); + $header_response = $split_action_response[0]; + $body_response = $split_action_response[1]; + $action_response_array = json_decode($body_response, true); + + $action_response_export = var_export($action_response_array, true); + error_log("----------------------------------------------\n" . + "FULL RESPONSE:\n\n{$action_response}\n\nEND FULL RESPONSE\n\n" . + "Headers:\n\n{$header_response}\n\nEnd Headers\n\n" . + "Data:\n\n{$body_response}\n\nEnd Data\n\n" . + "RESPONSE ARRAY:\n\n{$action_response_export}\n\nEND RESPONSE ARRAY\n" . + "----------------------------------------------"); + + unset($action_response); + unset($action_response_export); + + // Parse response, confirm HTTP response code etc. + $split_headers = explode("\r\n", $header_response); + if (substr($split_headers[0], 0, 9) == "HTTP/1.1 ") { + $split_http_response_line = explode(" ", $split_headers[0]); + if ($split_http_response_line[1] == "200") { + if ($http_method == "PUT") { + return true; + } else { + return $action_response_array['data']; + } + } else { + error_log("This API Request Failed.\n" . + "HTTP Response - {$split_http_response_line[1]}\n" . + "HTTP Error - {$split_headers[0]}"); + return false; + } + } else { + error_log("Error - Invalid HTTP Response.\n" . var_export($split_headers, true)); + return false; + } + + if ( ! empty($action_response_array['data'])) { + return $action_response_array['data']; + } else { + error_log("\$action_response_array['data'] is empty. Returning false.\n" . + var_export($action_response_array['data'], true)); + return false; + } + } + + /* + * array 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. + */ + public function reload_node_list() + { + $node_list = $this->get("/nodes"); + if (count($node_list) > 0) { + $nodes_array = []; + foreach ($node_list as $node) { + $nodes_array[] = $node['node']; + } + $this->cluster_node_list = $nodes_array; + return true; + } else { + error_log(" Empty list of nodes returned in this cluster."); + return false; + } + } + + /* + * array get_node_list () + * + */ + 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->cluster_node_list == null) { + if ($this->reload_node_list() === false) { + return false; + } + } + + return $this->cluster_node_list; + } + + /* + * bool|int get_next_vmid () + * Get Last VMID from a Cluster or a Node + * returns a VMID, or false if not found. + */ + public function get_next_vmid() + { + $vmid = $this->get("/cluster/nextid"); + if ($vmid == null) { + return false; + } else { + return $vmid; + } + } + + /* + * bool|string get_version () + * Return the version and minor revision of Proxmox Server + */ + public function get_version() + { + $version = $this->get("/version"); + if ($version == null) { + return false; + } else { + return $version['version']; + } + } + + /* + * object/array? get (string action_path) + */ + public function get($action_path, $parameters = []) + { + return $this->action($action_path, "GET", $parameters); + } + + /* + * bool put (string action_path, array parameters) + */ + public function put($action_path, $parameters = []) + { + return $this->action($action_path, "PUT", $parameters); + } + + /* + * bool post (string action_path, array parameters) + */ + public function post($action_path, $parameters = []) + { + return $this->action($action_path, "POST", $parameters); + } + + /* + * bool delete (string action_path) + */ + public function delete($action_path) + { + return $this->action($action_path, "DELETE"); + } + + // Logout not required, PVEAuthCookie tokens have a 2 hour lifetime. } - -?>