From 765bfbe2e8b3bf68ee0c35a73572ffdb1cb67082 Mon Sep 17 00:00:00 2001 From: Daniel Hunsaker Date: Mon, 2 Feb 2015 18:33:32 -0700 Subject: [PATCH 1/5] Add support for parameters to GET requests Addresses GitHub Issue #8 - Set parameters as CURLOPT_POSTFIELDS, which cURL will automatically translate into the URL query string. This lets us pass the ?params versus ¶ms logic off to cURL, which makes the code easier to maintain. - Allow all parameters passed to be empty by making the argument optional on GET, POST, and PUT. This is mostly for consistency. Signed-off-by: Daniel Hunsaker --- pve2_api.class.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) mode change 100755 => 100644 pve2_api.class.php diff --git a/pve2_api.class.php b/pve2_api.class.php old mode 100755 new mode 100644 index e2e2bb4..d52731b --- a/pve2_api.class.php +++ b/pve2_api.class.php @@ -176,7 +176,12 @@ private function action ($action_path, $http_method, $put_post_parameters = null // Lets decide what type of action we are taking... switch ($http_method) { case "GET": - // Nothing extra to do. + // Set "POST" data - cURL will translate this into the appropriate + // querystring so we don't have to worry about it. + $action_postfields_string = http_build_query($put_post_parameters); + curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $action_postfields_string); + unset($action_postfields_string); + break; case "PUT": curl_setopt($prox_ch, CURLOPT_CUSTOMREQUEST, "PUT"); @@ -334,21 +339,21 @@ public function get_version () { /* * object/array? get (string action_path) */ - public function get ($action_path) { - return $this->action($action_path, "GET"); + 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) { + 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) { + public function post ($action_path, $parameters = array()) { return $this->action($action_path, "POST", $parameters); } From 1a434ff420a5a7d95776b182f38b72cc8b671c26 Mon Sep 17 00:00:00 2001 From: Daniel Hunsaker Date: Tue, 3 Feb 2015 13:44:18 -0700 Subject: [PATCH 2/5] Fix cURL GET parameter passing Addresses GitHub issue #8 - cURL appears to not (or at least to no longer) actually support turning POSTFIELDS into query string parameters, so we have to build the query string ourselves. This update does just that. Signed-off-by: Daniel Hunsaker --- pve2_api.class.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pve2_api.class.php b/pve2_api.class.php index d52731b..9fb500e 100644 --- a/pve2_api.class.php +++ b/pve2_api.class.php @@ -176,10 +176,11 @@ private function action ($action_path, $http_method, $put_post_parameters = null // Lets decide what type of action we are taking... switch ($http_method) { case "GET": - // Set "POST" data - cURL will translate this into the appropriate - // querystring so we don't have to worry about it. + // 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); - curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $action_postfields_string); + $action_path .= (strpos($action_path, '?') === FALSE ? '?' : '&') . $action_postfields_string; unset($action_postfields_string); break; From 6f154f4c82b153432e445f730334f63192605711 Mon Sep 17 00:00:00 2001 From: Dan Hunsaker Date: Wed, 11 Feb 2015 12:59:26 -0700 Subject: [PATCH 3/5] Adjust for cURL ideosyncracies; use `if...else` instead of `? :` Signed-off-by: Dan Hunsaker --- pve2_api.class.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) mode change 100644 => 100755 pve2_api.class.php diff --git a/pve2_api.class.php b/pve2_api.class.php old mode 100644 new mode 100755 index 9fb500e..9efa8e0 --- a/pve2_api.class.php +++ b/pve2_api.class.php @@ -99,6 +99,8 @@ public function login () { return false; } + error_log("Login Ticket: {$login_ticket}"); + $login_ticket_data = json_decode($login_ticket, true); if ($login_ticket_data == null || $login_ticket_data['data'] == null) { // Login failed. @@ -169,7 +171,6 @@ private function action ($action_path, $http_method, $put_post_parameters = null // Prepare cURL resource. $prox_ch = curl_init(); - curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->hostname}:{$this->port}/api2/json{$action_path}"); $put_post_http_headers = array(); $put_post_http_headers[] = "CSRFPreventionToken: {$this->login_ticket['CSRFPreventionToken']}"; @@ -180,18 +181,21 @@ private function action ($action_path, $http_method, $put_post_parameters = null // 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); - $action_path .= (strpos($action_path, '?') === FALSE ? '?' : '&') . $action_postfields_string; + if ((strpos($action_path, '?') === FALSE) { + $action_path .= '?' . $action_postfields_string; + } else { + $action_path .= '&' . $action_postfields_string; + } unset($action_postfields_string); - break; case "PUT": - curl_setopt($prox_ch, CURLOPT_CUSTOMREQUEST, "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; @@ -207,8 +211,8 @@ private function action ($action_path, $http_method, $put_post_parameters = null 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. + curl_setopt($prox_ch, CURLOPT_CUSTOMREQUEST, "DELETE"); // Add required HTTP headers. curl_setopt($prox_ch, CURLOPT_HTTPHEADER, $put_post_http_headers); @@ -218,6 +222,7 @@ private function action ($action_path, $http_method, $put_post_parameters = null 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']); From 3c47315ccb23d0a7e2698ee50b19da75351e6710 Mon Sep 17 00:00:00 2001 From: Dan Hunsaker Date: Wed, 11 Feb 2015 13:45:13 -0700 Subject: [PATCH 4/5] Removing extraneous `error_log()` Signed-off-by: Dan Hunsaker --- pve2_api.class.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/pve2_api.class.php b/pve2_api.class.php index 9efa8e0..be7369c 100755 --- a/pve2_api.class.php +++ b/pve2_api.class.php @@ -99,8 +99,6 @@ public function login () { return false; } - error_log("Login Ticket: {$login_ticket}"); - $login_ticket_data = json_decode($login_ticket, true); if ($login_ticket_data == null || $login_ticket_data['data'] == null) { // Login failed. From 0adb4a7e5e9b393284a2de78e48262215531a173 Mon Sep 17 00:00:00 2001 From: Dan Hunsaker Date: Thu, 14 Jul 2016 16:35:21 -0600 Subject: [PATCH 5/5] Update README; Coding Standards --- README.md | 39 ++- TODO | 3 +- pve2_api.class.php | 711 +++++++++++++++++++++++---------------------- 3 files changed, 382 insertions(+), 371 deletions(-) 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. } - -?>