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.
+}
+
+?>