AbstractApi.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. <?php
  2. /*
  3. Copyright (c) 2012 Nathan Sullivan
  4. 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:
  5. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  6. 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.
  7. https://github.com/CpuID/pve2-api-php-client
  8. *
  9. */
  10. /* * ***********************************
  11. * EDITED BY MODULESGARDEN (2015-07-24)
  12. * *********************************** */
  13. namespace MGProvision\Proxmox\v2;
  14. abstract class AbstractApi
  15. {
  16. private $constructor_success;
  17. protected $pve_hostname;
  18. protected $pve_username;
  19. protected $pve_realm;
  20. protected $pve_password;
  21. private $print_debug;
  22. protected $pve_login_ticket;
  23. protected $pve_login_ticket_timestamp;
  24. protected $pve_cluster_node_list;
  25. protected $whmcsDebugMode = false;
  26. public $raw_message;
  27. public $port = '8006';
  28. protected $httpCode = '0';
  29. private static $moduleLogs = [];
  30. /**
  31. * FUNCTION __construct
  32. * Construct API object
  33. * @param string $pve_hostname
  34. * @param string $pve_username
  35. * @param string $pve_realm
  36. * @param string $pve_password
  37. */
  38. public function __construct($pve_hostname, $pve_username, $pve_realm, $pve_password)
  39. {
  40. if ($pve_realm == "")
  41. $pve_realm = "pam";
  42. if (strpos($pve_hostname, ':') !== false)
  43. {
  44. $ex = explode(":", $pve_hostname);
  45. $port = $ex['1'];
  46. $host = $ex['0'];
  47. if ($port && is_string($host))
  48. {
  49. $this->port = $port;
  50. $pve_hostname = $host;
  51. }
  52. }
  53. $this->pve_hostname = $pve_hostname;
  54. $this->pve_username = $pve_username;
  55. $this->pve_realm = $pve_realm;
  56. $this->pve_password = $pve_password;
  57. $this->print_debug = false;
  58. # Default this to null, so we can check later on if were logged in or not.
  59. $this->pve_login_ticket = null;
  60. $this->pve_login_ticket_timestamp = null;
  61. $this->pve_cluster_node_list = null;
  62. $this->constructor_success = true;
  63. }
  64. /**
  65. * FUNCTION constructor_success
  66. * Verify construct object
  67. * @return boolean $this->constructor_success
  68. */
  69. public function constructor_success()
  70. {
  71. return $this->constructor_success;
  72. }
  73. /**
  74. * FUNCTION convert_postfields_array_to_string
  75. * Convert postfields to string
  76. * @param array $postfields_array
  77. * @return string $postfields_string
  78. */
  79. private function convert_postfields_array_to_string($postfields_array)
  80. {
  81. $postfields_key_values = array();
  82. foreach ($postfields_array as $field_key => $field_value)
  83. {
  84. $postfields_key_values[] = urlencode($field_key) . "=" . urlencode($field_value);
  85. }
  86. $postfields_string = implode("&", $postfields_key_values);
  87. return $postfields_string;
  88. }
  89. /**
  90. * FUNCTION set_debug
  91. * Sets if we should print() debug information throughout the process,
  92. * to assist in troubleshooting...
  93. * @param boolean $on_off
  94. * @return boolean
  95. */
  96. public function set_debug($on_off)
  97. {
  98. if (is_bool($on_off))
  99. {
  100. $this->print_debug = $on_off;
  101. return true;
  102. }
  103. else
  104. {
  105. return false;
  106. }
  107. }
  108. /**
  109. * FUNCTION login
  110. * Performs login to PVE Server using JSON API, and obtains Access Ticket.
  111. * @return boolean
  112. */
  113. public function login()
  114. {
  115. if (!$this->constructor_success)
  116. {
  117. return false;
  118. }
  119. # Prepare login variables.
  120. $login_postfields = array();
  121. $login_postfields['username'] = $this->pve_username;
  122. $login_postfields['password'] = $this->pve_password;
  123. $login_postfields['realm'] = $this->pve_realm;
  124. $login_postfields_string = $this->convert_postfields_array_to_string($login_postfields);
  125. # Perform login request.
  126. $prox_ch = curl_init();
  127. curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->pve_hostname}:{$this->port}/api2/json/access/ticket");
  128. curl_setopt($prox_ch, CURLOPT_POST, true);
  129. curl_setopt($prox_ch, CURLOPT_RETURNTRANSFER, true);
  130. curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $login_postfields_string);
  131. curl_setopt($prox_ch, CURLOPT_SSL_VERIFYPEER, false);
  132. curl_setopt($prox_ch, CURLOPT_SSL_VERIFYHOST, false);
  133. curl_setopt($prox_ch, CURLOPT_CONNECTTIMEOUT, 30);
  134. $login_ticket = curl_exec($prox_ch);
  135. $err = curl_error($prox_ch);
  136. if ($login_ticket === false)
  137. {
  138. $err = $err ? $err : "Unable connect to Proxmox";
  139. throw new ProxmoxApiException($err);
  140. }
  141. curl_close($prox_ch);
  142. unset($prox_ch);
  143. unset($login_postfields_string);
  144. $login_ticket_data = json_decode($login_ticket, true);
  145. if ($this->whmcsDebugMode)
  146. {
  147. if (function_exists('logModuleCall'))
  148. {
  149. logModuleCall(
  150. "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)
  151. );
  152. self::$moduleLogs[] = [
  153. "proxmoxVPS",
  154. "https://{$this->pve_hostname}:{$this->port}/api2/json/access/ticket",
  155. print_r($login_postfields, true),
  156. '',
  157. print_r($login_ticket_data, true),
  158. [$this->pve_username, $this->pve_password]
  159. ];
  160. }
  161. }
  162. unset($login_postfields);
  163. if ($login_ticket_data == null || $login_ticket_data['data'] == null)
  164. {
  165. # Login failed.
  166. # Just to be safe, set this to null again.
  167. $this->pve_login_ticket_timestamp = null;
  168. throw new ProxmoxApiException(sprintf("Login to Proxmox host '%s' port '%s' failed", $this->pve_hostname, $this->port),401);
  169. return false;
  170. }
  171. else
  172. {
  173. # Login success.
  174. $this->pve_login_ticket = $login_ticket_data['data'];
  175. # We store a UNIX timestamp of when the ticket was generated here, so we can identify when we need
  176. # a new one expiration wise later on...
  177. $this->pve_login_ticket_timestamp = time();
  178. return true;
  179. }
  180. }
  181. /**
  182. * FUNCTION pve_check_login_ticket
  183. * Checks if the login ticket is valid still, returns false if not.
  184. * Method of checking is purely by age of ticket right now...
  185. * @return boolean
  186. */
  187. protected function pve_check_login_ticket()
  188. {
  189. if($this->pve_realm=="PVEAPIToken"){
  190. return true;
  191. }
  192. if ($this->pve_login_ticket == null)
  193. {
  194. # Just to be safe, set this to null again.
  195. $this->pve_login_ticket_timestamp = null;
  196. return false;
  197. }
  198. if ($this->pve_login_ticket_timestamp >= (time() + 7200))
  199. {
  200. # Reset login ticket object values.
  201. $this->pve_login_ticket = null;
  202. $this->pve_login_ticket_timestamp = null;
  203. return false;
  204. }
  205. else
  206. {
  207. return true;
  208. }
  209. }
  210. /**
  211. * FUNCTION pve_action
  212. * This method is responsible for the general cURL requests to the JSON API,
  213. * and sits behind the abstraction layer methods get/put/post/delete etc.
  214. * @param string $action_path
  215. * @param string $http_method
  216. * @param array $put_post_parameters
  217. * @return array
  218. */
  219. private function pve_action($action_path, $http_method, $put_post_parameters = null)
  220. {
  221. if (!$this->constructor_success)
  222. {
  223. return false;
  224. }
  225. # Check if we have a prefixed / on the path, if not add one.
  226. if (substr($action_path, 0, 1) != "/")
  227. {
  228. $action_path = "/" . $action_path;
  229. }
  230. if (!$this->pve_check_login_ticket())
  231. {
  232. if ($this->print_debug === true)
  233. {
  234. print("Error - Not logged into Proxmox Host. No Login Access Ticket found or Ticket Expired.\n");
  235. }
  236. return false;
  237. }
  238. # Prepare cURL resource.
  239. $prox_ch = curl_init();
  240. if ($this->print_debug === true)
  241. {
  242. print("\nURL - https://{$this->pve_hostname}:{$this->port}/api2/json" . $action_path . "\n");
  243. }
  244. #GET parameters
  245. $getparameters = null;
  246. if (!empty($put_post_parameters) && $http_method == "GET")
  247. {
  248. $getparameters = "?";
  249. foreach ($put_post_parameters as $k => $v)
  250. {
  251. $getparameters .= urlencode($k) . "=" . urlencode($v) . "&";
  252. }
  253. $getparameters = substr($getparameters, 0, -1);
  254. }
  255. $comand = explode("/", $action_path);
  256. $comand = end($comand);
  257. if ($comand == "rrd")
  258. {
  259. curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->pve_hostname}:{$this->port}/api2/png" . $action_path . $getparameters);
  260. }
  261. else
  262. {
  263. curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->pve_hostname}:{$this->port}/api2/json" . $action_path . $getparameters);
  264. }
  265. $put_post_http_headers = array();
  266. if($this->pve_realm=="PVEAPIToken"){
  267. $put_post_http_headers[] = sprintf("Authorization: PVEAPIToken=%s=%s", $this->pve_username, $this->pve_password);
  268. }else{
  269. $put_post_http_headers[] = "CSRFPreventionToken: " . $this->pve_login_ticket['CSRFPreventionToken'];
  270. }
  271. curl_setopt($prox_ch, CURLOPT_HEADER, true);
  272. # Lets decide what type of action we are taking...
  273. switch ($http_method)
  274. {
  275. case "GET":
  276. if($this->pve_realm=="PVEAPIToken"){
  277. curl_setopt($prox_ch, CURLOPT_HTTPHEADER, $put_post_http_headers);
  278. }
  279. # Nothing extra to do.
  280. break;
  281. case "PUT":
  282. curl_setopt($prox_ch, CURLOPT_CUSTOMREQUEST, "PUT");
  283. # Set "POST" data.
  284. $action_postfields_string = $this->convert_postfields_array_to_string($put_post_parameters);
  285. curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $action_postfields_string);
  286. unset($action_postfields_string);
  287. # Add required HTTP headers.
  288. curl_setopt($prox_ch, CURLOPT_HTTPHEADER, $put_post_http_headers);
  289. break;
  290. case "POST":
  291. curl_setopt($prox_ch, CURLOPT_POST, true);
  292. # Set POST data.
  293. $action_postfields_string = $this->convert_postfields_array_to_string($put_post_parameters);
  294. curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $action_postfields_string);
  295. unset($action_postfields_string);
  296. # Add required HTTP headers.
  297. curl_setopt($prox_ch, CURLOPT_HTTPHEADER, $put_post_http_headers);
  298. break;
  299. case "DELETE":
  300. curl_setopt($prox_ch, CURLOPT_CUSTOMREQUEST, "DELETE");
  301. # No "POST" data required, the delete destination is specified in the URL.
  302. # Add required HTTP headers.
  303. curl_setopt($prox_ch, CURLOPT_HTTPHEADER, $put_post_http_headers);
  304. break;
  305. default:
  306. if ($this->print_debug === true)
  307. {
  308. print("Error - Invalid HTTP Method specified.\n");
  309. }
  310. return false;
  311. }
  312. curl_setopt($prox_ch, CURLOPT_CONNECTTIMEOUT, 30);
  313. curl_setopt($prox_ch, CURLOPT_RETURNTRANSFER, true);
  314. if($this->pve_realm!="PVEAPIToken"){
  315. curl_setopt($prox_ch, CURLOPT_COOKIE, "PVEAuthCookie=" . $this->pve_login_ticket['ticket']);
  316. }
  317. curl_setopt($prox_ch, CURLOPT_SSL_VERIFYPEER, false);
  318. curl_setopt($prox_ch, CURLOPT_SSL_VERIFYHOST, false);
  319. $action_response = curl_exec($prox_ch);
  320. if(!$action_response ){
  321. $error = curl_error($prox_ch);
  322. $errorCode = curl_errno($prox_ch);
  323. }
  324. $this->httpCode = curl_getinfo($prox_ch, CURLINFO_HTTP_CODE);
  325. $this->raw_message = $action_response;
  326. curl_close($prox_ch);
  327. unset($prox_ch);
  328. // ModulesGarden
  329. if ($this->whmcsDebugMode) {
  330. if (function_exists('logModuleCall')) {
  331. logModuleCall(
  332. "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)
  333. );
  334. self::$moduleLogs[] = [
  335. "proxmoxVPS",
  336. $action_path,
  337. $http_method . " https://{$this->pve_hostname}:{$this->port}/api2/json" . $action_path . "\n" . print_r($put_post_parameters, true),
  338. '',
  339. sprintf("HTTP %s %s",$this->httpCode, $action_response),
  340. [$this->pve_username, $this->pve_password]
  341. ];
  342. }
  343. }
  344. if($error){
  345. throw new ProxmoxApiException($error, $errorCode);
  346. }
  347. $response = explode("\r\n\r\n", $action_response, 2);
  348. $header_response = explode("\r\n", $response[0], 2);
  349. $body_response = $response[1];
  350. $action_response_array = json_decode( $body_response , true);
  351. if ($comand == "rrd")
  352. {
  353. return $action_response;
  354. }
  355. # Parse response, confirm HTTP response code etc.
  356. if ($this->httpCode && in_array($this->httpCode,[403,500]) || $this->httpCode > 500 && empty( $action_response_array['data'])){
  357. $header_response[0] = str_replace("HTTP/1.1 {$this->httpCode}", "",$header_response[0] );
  358. $message = $header_response[0] ? ucfirst(trim($header_response[0])) : 'Wrong response from server';
  359. throw new ProxmoxApiException($message, $this->httpCode);
  360. }
  361. if ($this->httpCode)
  362. {
  363. if ($this->httpCode < 400 )
  364. {
  365. return $action_response_array['data'];
  366. }
  367. else
  368. {
  369. $errors = null;
  370. if (!empty($action_response_array['errors']))
  371. {
  372. $errors = "(";
  373. foreach ($action_response_array['errors'] as $k => $v)
  374. {
  375. $errors .= " [$k] - $v";
  376. }
  377. $errors .= " )";
  378. }
  379. return array('errors' => array( $errors));
  380. }
  381. } else
  382. {
  383. return array('errors' => array('Invalid HTTP Response - ' . print_r($action_response, true)));
  384. }
  385. if (!empty($action_response_array['data']))
  386. {
  387. return $action_response_array['data'];
  388. }
  389. }
  390. /**
  391. * FUNCTION whmcsDebugMode
  392. * Turn WHMCS DEBUG on
  393. * @author Grzegorz Draganik - ModulesGarden
  394. * @param bool $turnon
  395. */
  396. public function debug($turnon = true)
  397. {
  398. $this->whmcsDebugMode = $turnon;
  399. }
  400. /**
  401. * FUNCTION reload_node_list
  402. * Returns the list of node names as provided by /api2/json/nodes.
  403. * We need this for future get/post/put/delete calls.
  404. * ie. $this->get("nodes/XXX/status"); where XXX is one of the values from this return array.
  405. * @return boolean
  406. */
  407. public function reload_node_list()
  408. {
  409. if (!$this->constructor_success)
  410. {
  411. return false;
  412. }
  413. $node_list = $this->pve_action("/nodes", "GET");
  414. if (count($node_list) > 0)
  415. {
  416. $nodes_array = array();
  417. foreach ($node_list as $node)
  418. {
  419. $nodes_array[] = $node['node'];
  420. }
  421. $this->pve_cluster_node_list = $nodes_array;
  422. return true;
  423. }
  424. else
  425. {
  426. if ($this->print_debug === true)
  427. {
  428. print("Error - Empty list of nodes returned in this cluster.\n");
  429. }
  430. return false;
  431. }
  432. }
  433. /**
  434. * FUNCTION get_node_list
  435. * Geting node from proxmox server
  436. * @return array | boolean
  437. */
  438. public function get_node_list()
  439. {
  440. # We run this if we haven't queried for cluster nodes as yet, and cache it in the object.
  441. if ($this->pve_cluster_node_list == null)
  442. {
  443. if ($this->reload_node_list() === false)
  444. {
  445. return false;
  446. }
  447. }
  448. return $this->pve_cluster_node_list;
  449. }
  450. /**
  451. * FUNCTION get
  452. * GET request
  453. * @param string $action_path
  454. * @param array $parameters
  455. * @return array | boolean
  456. */
  457. public function get($action_path, $parameters = false)
  458. {
  459. if (!$this->constructor_success)
  460. {
  461. return false;
  462. }
  463. # We run this if we haven't queried for cluster nodes as yet, and cache it in the object.
  464. if ($this->pve_cluster_node_list == null)
  465. {
  466. if ($this->reload_node_list() === false)
  467. {
  468. return false;
  469. }
  470. }
  471. return $this->processRequest($this->pve_action($action_path, "GET", $parameters));
  472. }
  473. /**
  474. * FUNCTION put
  475. * PUT request
  476. * @param string $action_path
  477. * @param array $parameters
  478. * @return boolean | array
  479. */
  480. public function put($action_path, $parameters)
  481. {
  482. if (!$this->constructor_success)
  483. {
  484. return false;
  485. }
  486. # We run this if we haven't queried for cluster nodes as yet, and cache it in the object.
  487. if ($this->pve_cluster_node_list == null)
  488. {
  489. if ($this->reload_node_list() === false)
  490. {
  491. return false;
  492. }
  493. }
  494. return $this->processRequest($this->pve_action($action_path, "PUT", $parameters));
  495. }
  496. /**
  497. * FUNCTION post
  498. * POST request
  499. * @param string $action_path
  500. * @param string $parameters
  501. * @return boolean | array
  502. */
  503. public function post($action_path, $parameters = array())
  504. {
  505. if (!$this->constructor_success)
  506. {
  507. return false;
  508. }
  509. # We run this if we haven't queried for cluster nodes as yet, and cache it in the object.
  510. if ($this->pve_cluster_node_list == null)
  511. {
  512. if ($this->reload_node_list() === false)
  513. {
  514. return false;
  515. }
  516. }
  517. return $this->processRequest($this->pve_action($action_path, "POST", $parameters));
  518. }
  519. /**
  520. * FUNCTION delete
  521. * Delete request
  522. * @param string $action_path
  523. * @return boolean | array
  524. */
  525. public function delete($action_path) {
  526. if (!$this->constructor_success) {
  527. return false;
  528. }
  529. # We run this if we haven't queried for cluster nodes as yet, and cache it in the object.
  530. if ($this->pve_cluster_node_list == null) {
  531. if ($this->reload_node_list() === false) {
  532. return false;
  533. }
  534. }
  535. return $this->processRequest($this->pve_action($action_path, "DELETE"));
  536. }
  537. public function isProxmox4()
  538. {
  539. if (empty($this->_version))
  540. {
  541. $info = $this->get('/version');
  542. $this->_version = $info ['version'];
  543. }
  544. return version_compare($this->_version, "4.0", '>=');
  545. }
  546. abstract function processRequest($response);
  547. # Logout not required, PVEAuthCookie tokens have a 2 hour lifetime.
  548. public static function beginTransaction(){
  549. self::$moduleLogs=[];
  550. }
  551. public static function commit(){
  552. foreach (self::$moduleLogs as &$log){
  553. logModuleCall(
  554. $log[0], $log[1], $log[2], $log[3], $log[4], $log[5]
  555. );
  556. unset($log);
  557. }
  558. }
  559. public function getPveHostname(){
  560. return $this->pve_hostname;
  561. }
  562. /**
  563. * @return mixed
  564. */
  565. public function getTokenId()
  566. {
  567. return $this->tokenId;
  568. }
  569. /**
  570. * @param mixed $tokenId
  571. * @return AbstractApi
  572. */
  573. public function setTokenId($tokenId)
  574. {
  575. $this->tokenId = $tokenId;
  576. return $this;
  577. }
  578. }