Index: html/pages/device/proxmox.inc.php =================================================================== --- html/pages/device/proxmox.inc.php (revision 0) +++ html/pages/device/proxmox.inc.php (revision 0) @@ -0,0 +1,81 @@ + 'device', + 'device' => $device['device_id'], + 'tab' => 'proxmox'); + +$navbar['brand'] = "Guest"; +$navbar['class'] = "navbar-narrow"; + +// Navbar content is static +$navbar['options'] = array( + 'cpu' => array( + 'url' => generate_url(array('page' => 'device', 'device' => $device['device_id'], 'tab' => 'proxmox', 'group' => 'cpu')), + 'text' => "CPU" + ), + 'mem' => array( + 'url' => generate_url(array('page' => 'device', 'device' => $device['device_id'], 'tab' => 'proxmox', 'group' => 'mem')), + 'text' => "Memory" + ), + 'diskusage' => array( + 'url' => generate_url(array('page' => 'device', 'device' => $device['device_id'], 'tab' => 'proxmox', 'group' => 'diskusage')), + 'text' => "Disk Usage" + ), + 'diskio' => array( + 'url' => generate_url(array('page' => 'device', 'device' => $device['device_id'], 'tab' => 'proxmox', 'group' => 'diskio')), + 'text' => "Disk I/O" + ), + 'netio' => array( + 'url' => generate_url(array('page' => 'device', 'device' => $device['device_id'], 'tab' => 'proxmox', 'group' => 'netio')), + 'text' => "Network Traffic" + ) +); + +// Default group is 'CPU' +$group = isset($vars['group']) ? $vars['group']:"cpu"; +// Make chosen navbar button look pressed +$navbar['options'][$group]['class'] = "active"; + +// Draw navbar +print_navbar($navbar); + +// We will read all known guests for this device +// into this array and then sort them by guest vmid +$guests = array(); + +// Fetch all guests known by this device +// (should be all guests in the cluster) +foreach (dbFetchRows("SELECT * FROM proxmox WHERE device_id = ? ORDER BY guest_id ASC", array($device['device_id'])) as $guest) +{ + // Store guest info for subsequent sorting + $guests[$guest['guest_id']] = $guest; +} +// Sort guests to be displayed +// in ascending vmid order +ksort($guests); + +// Finally, display all guest graphs for chosen group +foreach ($guests as $vmid => $guest) +{ + // Variable expected by the print-device-graph.php: + // - title that will be displayed above each graph row + $graph_title = ($guest['guest_type'] == "openvz" ? "OpenVZ Container":"KVM Virtual machine")." ".$vmid." (".$guest['guest_name'].")"; + // Array expected by the print-device-graph.php: + // - mandatory parameters that will be passed to graph.php + // and subsequently to our graph generating code when + // when invoked from tag + $graph_array['type'] = "proxmox_".$group; + $graph_array['device'] = $device['device_id']; + $graph_array['group'] = $group; + // - our custom parameters that will be passed to graph.php + // and subsequently to our graph generating code when + // invoked from tag + $graph_array['proxmox_id'] = $guest['proxmox_id']; + + // Generate tags on current page + include("includes/print-device-graph.php"); +} + +$pagetitle[] = "Graphs"; + +?> Index: html/pages/device.inc.php =================================================================== --- html/pages/device.inc.php (revision 4735) +++ html/pages/device.inc.php (working copy) @@ -198,6 +198,12 @@ $navbar['options']['vm'] = array('text' => 'VMs', 'icon' => 'oicon-network-cloud'); } + // Print Proxmox VE Guests tab if there are matching entries in the proxmox table + if (dbFetchCell("SELECT COUNT(guest_id) FROM proxmox WHERE device_id = '" . $device["device_id"] . "'") > '0') + { + $navbar['options']['proxmox'] = array('text' => 'Proxmox VE Guests',icon => 'oicon-network-cloud'); + } + // $loadbalancer_tabs is used in device/loadbalancer/ to build the submenu. we do it here to save queries // Check for Netscaler vservers and services Index: html/includes/graphs/proxmox/auth.inc.php =================================================================== --- html/includes/graphs/proxmox/auth.inc.php (revision 0) +++ html/includes/graphs/proxmox/auth.inc.php (revision 0) @@ -0,0 +1,23 @@ + 'CPU', + 'mem' => 'Memory', + 'diskusage' => 'Disk Usage', + 'diskio' => 'Disk I/O', + 'netio' => 'Network Traffic' + ); + // Graph's RRD file name + $rrd_filename = $config['rrd_dir']."/proxmox/".$proxmox['cluster_name']."/".$proxmox['guest_id']."/".$graphtype['subtype'].".rrd"; + // Graph's page title + $title = generate_device_link($device)." :: ".overlib_link(generate_device_url($device, array('tab' => 'proxmox')), "Proxmox VE Guests")." :: ".overlib_link(generate_device_url($device, array('tab' => 'proxmox', 'group' => $graphtype['subtype'])), $group_names[$graphtype['subtype']])." :: ".($proxmox['guest_type'] == "openvz" ? "Container":"Virtual Machine")." ".$proxmox['guest_id']." (".$proxmox['guest_name'].")"; + $auth = TRUE; + } + + +?> \ No newline at end of file Index: html/includes/graphs/proxmox/diskio.inc.php =================================================================== --- html/includes/graphs/proxmox/diskio.inc.php (revision 0) +++ html/includes/graphs/proxmox/diskio.inc.php (revision 0) @@ -0,0 +1,10 @@ + \ No newline at end of file Index: html/includes/graphs/proxmox/cpu.inc.php =================================================================== --- html/includes/graphs/proxmox/cpu.inc.php (revision 0) +++ html/includes/graphs/proxmox/cpu.inc.php (revision 0) @@ -0,0 +1,20 @@ + Index: html/includes/graphs/proxmox/mem.inc.php =================================================================== --- html/includes/graphs/proxmox/mem.inc.php (revision 0) +++ html/includes/graphs/proxmox/mem.inc.php (revision 0) @@ -0,0 +1,58 @@ += "300") + { + $rrd_options .= " --vertical-label bytes".$labelsize; + } + $rrd_options .= " DEF:max=".$rrd_filename.":max:MAX"; + $rrd_options .= " DEF:used=".$rrd_filename.":used:AVERAGE"; + $rrd_options .= " DEF:min=".$rrd_filename.":min:MIN"; + $rrd_options .= " CDEF:x=used,0,*"; + $rrd_options .= " COMMENT:\"\t".$pad."Now".$pad2."Avg".$pad2."Max\l\""; + $rrd_options .= " COMMENT:'\l'"; + $rrd_options .= " AREA:max#70c050"; + $rrd_options .= " AREA:used#008000:\"In use\""; + $rrd_options .= " GPRINT:used:LAST:%6.2lf%s"; + $rrd_options .= " GPRINT:used:AVERAGE:%6.2lf%s"; + $rrd_options .= " GPRINT:used:MAX:%6.2lf%s\\n"; + $rrd_options .= " AREA:x#70c050:\"Total \""; + $rrd_options .= " GPRINT:max:LAST:%6.2lf%s\\n"; + if ($vars['guesttype'] == 'qemu') + { + $rrd_options .= " COMMENT:\" \"\\n"; + $rrd_options .= " LINE2:min#ff0000:\"Balloon minimum size\""; + $rrd_options .= " GPRINT:min:LAST:%6.2lf%s\\n"; + } + if ($_GET['previous'] == 'yes') + { + $rrd_options .= " COMMENT:\" \"\\n"; + $rrd_options .= " COMMENT:\"Previous\"\\n"; + $rrd_options .= " DEF:maxX=".$rrd_filename.":max:MAX:start=".$prev_from.":end=".$from; + $rrd_options .= " DEF:usedX=".$rrd_filename.":used:AVERAGE:start=".$prev_from.":end=".$from; + $rrd_options .= " SHIFT:maxX:$period"; + $rrd_options .= " SHIFT:usedX:$period"; + $rrd_options .= " AREA:maxX#66800080"; + $rrd_options .= " LINE2:usedX#004444:\"In use\""; + $rrd_options .= " GPRINT:usedX:LAST:%6.2lf%s"; + $rrd_options .= " GPRINT:usedX:AVERAGE:%6.2lf%s"; + $rrd_options .= " GPRINT:usedX:MAX:%6.2lf%s\\n"; + $rrd_options .= " AREA:x#668000:\"Total \""; + $rrd_options .= " GPRINT:maxX:LAST:%6.2lf%s\\n"; + } + +?> Index: html/includes/graphs/proxmox/netio.inc.php =================================================================== --- html/includes/graphs/proxmox/netio.inc.php (revision 0) +++ html/includes/graphs/proxmox/netio.inc.php (revision 0) @@ -0,0 +1,11 @@ + Index: html/includes/graphs/proxmox/diskusage.inc.php =================================================================== --- html/includes/graphs/proxmox/diskusage.inc.php (revision 0) +++ html/includes/graphs/proxmox/diskusage.inc.php (revision 0) @@ -0,0 +1,51 @@ += "300") + { + $rrd_options .= " --vertical-label bytes".$labelsize; + } + $rrd_options .= " DEF:max=".$rrd_filename.":max:MAX"; + $rrd_options .= " DEF:used=".$rrd_filename.":used:AVERAGE"; + $rrd_options .= " CDEF:x=used,0,*"; + $rrd_options .= " COMMENT:\"\t".$pad."Now".$pad2."Avg".$pad2."Max\l\""; + $rrd_options .= " COMMENT:'\l'"; + $rrd_options .= " AREA:max#6080d0"; + $rrd_options .= " AREA:used#004080:\"In use\""; + $rrd_options .= " GPRINT:used:LAST:%6.2lf%s"; + $rrd_options .= " GPRINT:used:AVERAGE:%6.2lf%s"; + $rrd_options .= " GPRINT:used:MAX:%6.2lf%s\\n"; + $rrd_options .= " AREA:x#6080d0:\"Total \""; + $rrd_options .= " GPRINT:max:LAST:%6.2lf%s\\n"; + if ($_GET['previous'] == 'yes') + { + $rrd_options .= " COMMENT:\" \"\\n"; + $rrd_options .= " COMMENT:\"Previous\"\\n"; + $rrd_options .= " DEF:maxX=".$rrd_filename.":max:MAX:start=".$prev_from.":end=".$from; + $rrd_options .= " DEF:usedX=".$rrd_filename.":used:AVERAGE:start=".$prev_from.":end=".$from; + $rrd_options .= " SHIFT:maxX:$period"; + $rrd_options .= " SHIFT:usedX:$period"; + $rrd_options .= " AREA:maxX#66008080"; + $rrd_options .= " LINE2:usedX#440044:\"In use\""; + $rrd_options .= " GPRINT:usedX:LAST:%6.2lf%s"; + $rrd_options .= " GPRINT:usedX:AVERAGE:%6.2lf%s"; + $rrd_options .= " GPRINT:usedX:MAX:%6.2lf%s\\n"; + $rrd_options .= " AREA:x#660080:\"Total \""; + $rrd_options .= " GPRINT:maxX:LAST:%6.2lf%s\\n"; + } + +?> Index: includes/polling/proxmox.inc.php =================================================================== --- includes/polling/proxmox.inc.php (revision 0) +++ includes/polling/proxmox.inc.php (revision 0) @@ -0,0 +1,142 @@ +constructor_success()) + { + // Attempt login to Proxmox host + if ($pve2->login()) + { + // This is where we keep per-cluster Proxmox guest stats + $cluster_dir = $config['rrd_dir']."/proxmox/".$cluster_name; + // If not present, create it + if (!is_dir($cluster_dir)) + { + mkdir($cluster_dir, 0777, TRUE); + } + + // Get list of KVM guests and VZ containers per node + foreach (dbFetchRows("SELECT * FROM proxmox WHERE cluster_name = ? AND device_id = ?", array($cluster_name, $device['device_id'])) as $discovered_guest) + { + // Query Proxmox node for this guest's data + $guest = $pve2->get("/nodes/".$discovered_guest['node_name']."/".$discovered_guest['guest_type']."/".$discovered_guest['guest_id']."/status/current"); + + if ($debug) + { + echo("VMID ".$discovered_guest['guest_id'].": "); + print_r($guest); + } + + // Guest-specific RRD dir + $guest_dir = $cluster_dir."/".$discovered_guest['guest_id']; + // If not present, create it + if (!is_dir($guest_dir)) + { + mkdir($guest_dir, 0777, TRUE); + } + + // This is where we keep guest CPU stats + $cpu_rrd = $guest_dir."/cpu.rrd"; + if (!is_file($cpu_rrd)) + { + rrdtool_create($cpu_rrd, "--step 300 DS:load:GAUGE:600:0:U ".$config['rrd_rra']); + if (!is_file($cpu_rrd)) + { + echo("Failed to create RRD file for CPU stats for guest ".$guest['name']." [vmid ".$guest['vmid']."] while polling Proxmox host ".$device['hostname']."\n"); + logfile("Failed to create RRD file for memory stats for guest ".$guest['name']." [vmid ".$guest['vmid']."] while polling Proxmox host ".$device['hostname']); + } + } + // Update CPU stats + rrdtool_update($cpu_rrd,"N:".$guest['cpu']); + + // This is where we keep guest mem stats + $mem_rrd = $guest_dir."/mem.rrd"; + if (!is_file($mem_rrd)) + { + rrdtool_create($mem_rrd, "--step 300 DS:max:GAUGE:600:0:U DS:used:GAUGE:600:0:U DS:min:GAUGE:600:0:U ".$config['rrd_rra']); + if (!is_file($mem_rrd)) + { + echo("Failed to create RRD file for memory stats for guest ".$guest['name']." [vmid ".$guest['vmid']."] while polling Proxmox host ".$device['hostname']."\n"); + logfile("Failed to create RRD file for memory stats for guest ".$guest['name']." [vmid ".$guest['vmid']."] while polling Proxmox host ".$device['hostname']); + } + } + // Update memory stats + rrdtool_update($mem_rrd,"N:".$guest['maxmem'].":".$guest['mem'].":".(isset($guest['balloon_min']) ? $guest['balloon_min']:"0")); + + // This is where we keep guest disk usage stats + $diskusage_rrd = $guest_dir."/diskusage.rrd"; + if (!is_file($diskusage_rrd)) + { + rrdtool_create($diskusage_rrd, "--step 300 DS:max:GAUGE:600:0:U DS:used:GAUGE:600:0:U ".$config['rrd_rra']); + if (!is_file($diskusage_rrd)) + { + echo("Failed to create RRD file for disk usage stats for guest ".$guest['name']." [vmid ".$guest['vmid']."] while polling Proxmox host ".$device['hostname']."\n"); + logfile("Failed to create RRD file for disk usage stats for guest ".$guest['name']." [vmid ".$guest['vmid']."] while polling Proxmox host ".$device['hostname']); + } + } + // Update disk usage stats + rrdtool_update($diskusage_rrd,"N:".$guest['maxdisk'].":".$guest['disk']); + + // This is where we keep guest disk I/O stats + $diskio_rrd = $guest_dir."/diskio.rrd"; + if (!is_file($diskio_rrd)) + { + rrdtool_create($diskio_rrd, "--step 300 DS:read:COUNTER:600:0:U DS:write:COUNTER:600:0:U ".$config['rrd_rra']); + if (!is_file($diskio_rrd)) + { + echo("Failed to create RRD file for disk I/O stats for guest ".$guest['name']." [vmid ".$guest['vmid']."] while polling Proxmox host ".$device['hostname']."\n"); + logfile("Failed to create RRD file for disk I/O stats for guest ".$guest['name']." [vmid ".$guest['vmid']."] while polling Proxmox host ".$device['hostname']); + } + } + // Update disk I/O stats + rrdtool_update($diskio_rrd,"N:".$guest['diskread'].":".$guest['diskwrite']); + + // This is where we keep guest network stats + $netio_rrd = $guest_dir."/netio.rrd"; + if (!is_file($netio_rrd)) + { + rrdtool_create($netio_rrd, "--step 300 DS:in:COUNTER:600:0:U DS:out:COUNTER:600:0:U ".$config['rrd_rra']); + if (!is_file($netio_rrd)) + { + echo("Failed to create RRD file for network I/O stats for guest ".$guest['name']." [vmid ".$guest['vmid']."] while polling Proxmox host ".$device['hostname']."\n"); + logfile("Failed to create RRD file for network I/O stats for guest ".$guest['name']." [vmid ".$guest['vmid']."] while polling Proxmox host ".$device['hostname']); + } + } + // Update network I/O stats + rrdtool_update($netio_rrd,"N:".$guest['netin'].":".$guest['netout']); + } + + } else { + echo("Login to Proxmox host ".$device['hostname']." failed"); + logfile("Login to Proxmox host ".$device['hostname']." failed"); + } + } else { + echo("PVE2 API failed to connect to host ".$device['hostname']); + logfile("PVE2 API failed to connect to host ".$device['hostname']); + } + } + + unset($pve2, $row); + + echo("\n"); +} + +?> Index: includes/defaults.inc.php =================================================================== --- includes/defaults.inc.php (revision 4735) +++ includes/defaults.inc.php (working copy) @@ -565,6 +565,11 @@ $config['enable_libvirt'] = 0; // Enable Libvirt VM support $config['libvirt_protocols'] = array("qemu+ssh","xen+ssh"); // Mechanisms used, add or remove if not using this on any of your machines. +// Proxmox VE +$config['proxmox']['realm'] = ""; // can be 'pam' or 'pve' +$config['proxmox']['username'] = ""; +$config['proxmox']['password'] = ""; + // Unix Agent settings $config['unix-agent']['port'] = 36602; // Default agent port @@ -683,6 +688,7 @@ $config['poller_modules']['applications'] = 1; $config['poller_modules']['fdb-table'] = 1; $config['poller_modules']['wmi'] = 0; +$config['poller_modules']['proxmox'] = 1; // List of discovery modules. Need to be in this array to be // considered for execution. @@ -713,6 +719,7 @@ $config['discovery_modules']['toner'] = 1; $config['discovery_modules']['ucd-diskio'] = 1; $config['discovery_modules']['services'] = 1; +$config['discovery_modules']['proxmox'] = 1; // Simple Observium API Settings Index: includes/discovery/proxmox.inc.php =================================================================== --- includes/discovery/proxmox.inc.php (revision 0) +++ includes/discovery/proxmox.inc.php (revision 0) @@ -0,0 +1,176 @@ + $device_id, + 'node_name' => $node_name, + 'guest_name' => $guest['name'], + 'guest_type' => $guest['type']); + // Update guest info + $res = dbUpdate($params, 'proxmox', "cluster_name = ? AND guest_id = ?", array($cluster_name, $guest['vmid'])); + if ($debug) + { + echo(($res ? "Updated":"Failed to update")." guest ".$guest['vmid']." info\n"); + } else { + echo("."); + } + // Guest info hasn't changed since last discovery + } else { + if ($debug) + { + echo("Guest ".$guest['vmid']." info hasn't changed, update not needed\n"); + } else { + echo("."); + } + } + // If guest isn't known, add it + } else { + // Prepare query params + $params = array('device_id' => $device_id, + 'node_name' => $node_name, + 'cluster_name' => $cluster_name, + 'guest_id' => $guest['vmid'], + 'guest_name' => $guest['name'], + 'guest_type' => $guest['type']); + // Add newly discovered guest + $res = dbInsert($params, 'proxmox'); + if ($debug) + { + echo((($res === false) ? "Failed to insert":"Inserted")." guest ".$guest['vmid']." info\n"); + } else { + echo("+"); + } + } + } + + } + + // realm above can be pve, pam or any other realm available. + $pve2 = new PVE2_API($device['hostname'], $config['proxmox']['username'], $config['proxmox']['realm'], $config['proxmox']['password']); + // Check if we have API object + if ($pve2->constructor_success()) + { + // Attempt login to Proxmox host + if ($pve2->login()) + { + // Get cluster.conf data + $cluster_conf = $pve2->get("/cluster/ha/config/"); + // Proxmox cluster name this node belongs to + $cluster_name = $cluster_conf['children'][0]['name']; + // If we don't have the cluster name, we will assume + // Proxmox node is working in standalone mode and + // use device's hostname as cluster name. + if (!isset($cluster_name) || empty($cluster_name)) + { + if ($debug) echo("Cluster name is missing. Assuming standalone mode.\n"); + $cluster_name = $device['hostname']; + } + // The list of discovered guest VMIDs in this cluster + $discovered_guests_ids = array(); + // The list of previosly discovered guests in this cluster + $previosly_discovered_guests = array(); + + if ($debug) echo("Fetching already known guests:\n"); + + // Create the lookup table of known guests + foreach (dbFetchRows("SELECT * FROM proxmox WHERE cluster_name = ? AND device_id = ?", array($cluster_name, $device['device_id'])) as $guest) + { + $previosly_discovered_guests[$guest['guest_id']] = $guest; + if ($debug) + { + echo("VMID ".$guest['guest_id'].": "); + print_r($guest); + } + } + + // Go through all nodes in the cluster and discover guests + foreach ($pve2->get_node_list() as $node_name) + { + // Build the list of KVM guests + foreach ($pve2->get("/nodes/".$node_name."/qemu/") as $vm) + { + // Skip templates + if (!$vm['template']) + { + if ($debug) + { + echo("Discovered KVM virtual machine ".$vm['vmid'].": "); + print_r($vm); + } + // Guest type is KVM virtual machine + $vm['type'] = 'qemu'; + // Store Proxmox guest info + update_guest_info($device['device_id'], $node_name, $cluster_name, $vm, $previosly_discovered_guests); + // Maintain list of discovered VMIDs + array_push($discovered_guests_ids, $vm['vmid']); + } + } + // Build the list of OpenVZ containers + foreach ($pve2->get("/nodes/".$node_name."/openvz/") as $vz) + { + if ($debug) + { + echo("Discovered OpenVZ container ".$vz['vmid'].": "); + print_r($vm); + } + // Guest type is OpenVZ container + $vz['type'] = 'openvz'; + // Store Proxmox guest info + update_guest_info($device['device_id'], $node_name, $cluster_name, $vz, $previosly_discovered_guests); + // Maintain list of discovered VMIDs + array_push($discovered_guests_ids, $vz['vmid']); + } + } + + // Remove old guest entries from the database, if any + if (count($discovered_guests_ids)) + { + $num_deleted = dbDelete('proxmox', "cluster_name = ? AND device_id = ? AND guest_id NOT IN (".implode(",", array_values($discovered_guests_ids)).")", array($cluster_name, $device['device_id'])); + if ($debug) echo("Deleted ".$num_deleted." stale guest entries"); + } + + unset($discovered_guests_ids, $previosly_discovered_guests); + + } else { + if ($debug) echo("Login to Proxmox host ".$device['hostname']." failed"); + } + } else { + if ($debug) echo("PVE2 API couldn't connect to host ".$device['hostname']); + } + + unset($pve2); + + echo("\n"); +} + +?> Index: includes/pve2/pve2_api.class.php =================================================================== --- includes/pve2/pve2_api.class.php (revision 0) +++ includes/pve2/pve2_api.class.php (revision 0) @@ -0,0 +1,419 @@ +constructor_success = false; + return false; + } + # Check hostname resolves. + if (gethostbyname($pve_hostname) == $pve_hostname && !filter_var($pve_hostname, FILTER_VALIDATE_IP)) { + # TODO - better error handling? + print("Cannot resolve ".$pve_hostname.", exiting.\n"); + $this->constructor_success = false; + return false; + } + + $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; + } + + public function constructor_success () { + return $this->constructor_success; + } + + 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; + } + + /* + * bool set_debug (bool on_off) + * Sets if we should print() debug information throughout the process, + * to assist in troubleshooting... + */ + public function set_debug ($on_off) { + if (is_bool($on_off)) { + $this->print_debug = $on_off; + return true; + } else { + return false; + } + } + + /* + * bool login () + * Performs login to PVE Server using JSON API, and obtains Access Ticket. + */ + 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); + unset($login_postfields); + + # Perform login request. + $prox_ch = curl_init(); + curl_setopt($prox_ch, CURLOPT_URL, "https://".$this->pve_hostname.":8006/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); + + $login_ticket = curl_exec($prox_ch); + + curl_close($prox_ch); + unset($prox_ch); + unset($login_postfields_string); + + $login_ticket_data = json_decode($login_ticket, true); + if ($login_ticket_data == null) { + # Login failed. + # Just to be safe, set this to null again. + $this->pve_login_ticket_timestamp = null; + 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; + } + } + + /* + * bool 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... + */ + protected function pve_check_login_ticket () { + 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; + } + } + + /* + * object pve_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 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.":8006/api2/json".$action_path."\n"); + } + curl_setopt($prox_ch, CURLOPT_URL, "https://".$this->pve_hostname.":8006/api2/json".$action_path); + + $put_post_http_headers = array(); + $put_post_http_headers[] = "CSRFPreventionToken: ".$this->pve_login_ticket['CSRFPreventionToken']; + # Lets decide what type of action we are taking... + switch ($http_method) { + case "GET": + # 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_HEADER, true); + curl_setopt($prox_ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($prox_ch, CURLOPT_COOKIE, "PVEAuthCookie=".$this->pve_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]; + + if ($this->print_debug === true) { + print("----------------------------------------------\n"); + + print("\nFULL RESPONSE:\n\n"); + print($action_response); + print("\n\nEND FULL RESPONSE.\n"); + + print("\nHeaders:\n\n"); + print($header_response); + print("\n\nEnd Headers.\n"); + + print("\nData:\n\n"); + print($body_response); + print("\n\nEnd Headers.\n"); + } + + $action_response_array = json_decode($body_response, true); + if ($this->print_debug === true) { + print("\nRESPONSE ARRAY:\n\n"); + print_r($action_response_array); + print("\nEND RESPONSE ARRAY.\n"); + print("----------------------------------------------\n"); + } + + unset($action_response); + + # 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 { + if ($this->print_debug === true) { + print("This API Request Failed.\n"); + print("HTTP Response - ".$split_http_response_line[1]."\n"); + print("HTTP Error - ".$split_headers[0]."\n"); + } + return false; + } + } else { + if ($this->print_debug === true) { + print("Error - Invalid HTTP Response.\n"); + print_r($split_headers); + print("\n"); + } + return false; + } + + if (!empty($action_response_array['data'])) { + return $action_response_array['data']; + } else { + if ($this->print_debug === true) { + print("Error - \$action_response_array['data'] is empty. Returning false.\n"); + var_dump($action_response_array['data']); + print("\n"); + } + return false; + } + } + + /* + * array get_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 () { + 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; + } + } + + 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; + } + + /* + * object/array? get (string action_path) + */ + public function get ($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->pve_action($action_path, "GET"); + } + + /* + * bool put (string action_path, array parameters) + */ + 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->pve_action($action_path, "PUT", $parameters); + } + + /* + * bool post (string action_path, array parameters) + */ + public function post ($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->pve_action($action_path, "POST", $parameters); + } + + /* + * bool delete (string action_path) + */ + 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->pve_action($action_path, "DELETE"); + } + + # Logout not required, PVEAuthCookie tokens have a 2 hour lifetime. +} + +?>