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 @@
+<?php
+
+$link_array = array('page'    => '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 <img src="graph.php? ..."/> 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 <img src="graph.php? ..."/> tag
+  $graph_array['proxmox_id'] = $guest['proxmox_id'];
+  
+  // Generate <img src="graph.php? ..."/> 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 @@
+<?php
+$proxmox = dbFetchRow("SELECT * FROM `proxmox` where `proxmox_id` = ?", array($vars['proxmox_id']));
+
+if (is_numeric($proxmox['device_id']) && ($auth || device_permitted($proxmox['device_id'])))
+{
+    // Pretty names for stat groups
+    // (used in graph's page title)
+    $group_names = array(
+      'cpu' => '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 @@
+<?php
+
+$ds_in = "read";
+$ds_out = "write";
+
+$format = "bytes";
+
+include("includes/graphs/generic_data.inc.php");
+
+?>
\ 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 @@
+<?php
+
+$descr = "VMID " . $proxmox['guest_id'] . " (".$proxmox['guest_name'].")";
+
+$colour_line = "cc0000";
+$colour_area = "FFBBBB";
+$colour_minmax = "c5c5c5";
+
+$multiplier = 100;
+$unit_text = "CPU Util %";
+
+$units = '%';
+$ds="load";
+
+$scale_min = "0";
+$scale_max = "100";
+
+include($config['html_dir']."/includes/graphs/generic_simplex.inc.php");
+
+?>
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 @@
+<?php
+
+include("includes/graphs/common.inc.php");
+
+// Some very unintelligent graph legend alignment
+if ($width < "300")
+{
+  $pad = "    ";
+  $pad2 = "      ";
+//  $labelsize = " --font UNIT:5";
+} else {
+  $pad = "     ";
+  $pad2 = "      ";
+  $labelsize = "";
+  $rrd_options .= " --title \"VMID " . $proxmox['guest_id'] . " (".$proxmox['guest_name'].")\"";
+}
+
+    if ($width >= "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 @@
+<?php
+
+$rrd_filename = $rrd_filename;
+$ds_in = "in";
+$ds_out = "out";
+$format = "bytes";
+$graph_max = 1;
+
+include($config['html_dir']."/includes/graphs/generic_data.inc.php");
+
+?>
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 @@
+<?php
+
+include("includes/graphs/common.inc.php");
+
+// Some very unintelligent graph legend alignment
+if ($width < "300")
+{
+  $pad = "    ";
+  $pad2 = "      ";
+//  $labelsize = " --font UNIT:5";
+} else {
+  $pad = "     ";
+  $pad2 = "      ";
+  $labelsize = "";
+  $rrd_options .= " --title \"VMID " . $proxmox['guest_id'] . " (".$proxmox['guest_name'].")\"";
+}
+
+    if ($width >= "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 @@
+<?php
+
+global $debug;
+
+if ($device['os'] == "linux")
+{
+  echo("Proxmox VE: ");
+
+  // Check if this device is running Proxmox guests
+  $row = dbFetchRow("SELECT cluster_name FROM proxmox WHERE device_id = ? LIMIT 1", array($device['device_id']));
+  if (isset($row) && isset($row['cluster_name']) && !empty($row['cluster_name']))
+  {
+    // Proxmox cluster name this node belongs to
+    $cluster_name = $row['cluster_name'];
+
+    // Include PVE2 API client code taken from
+    // https://github.com/CpuID/pve2-api-php-client
+    require_once("includes/pve2/pve2_api.class.php");
+
+    // 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())
+      {
+        // 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 @@
+<?php
+
+global $debug;
+
+if ($device['os'] == "linux")
+{
+  echo("Proxmox VE: ");
+
+  // Include PVE2 API client code taken from
+  // https://github.com/CpuID/pve2-api-php-client
+  require_once("includes/pve2/pve2_api.class.php");
+
+  // Define this function only once to prevent breaking
+  // the discovery process due to function redefinition
+  if (!function_exists('update_guest_info')) {
+
+    // This function updates the database with discovered info
+    function update_guest_info($device_id, $node_name, $cluster_name, $guest, $prev_guests)
+    {
+      global $debug;
+
+      // Previous instance of guest with this VMID
+      $prev_guest = $prev_guests[$guest['vmid']];
+
+      // If guest is already known ...
+      if (isset($prev_guest) && is_array($prev_guest))
+      {
+        // ... but info differs, update it
+        if ($prev_guest['device_id'] != $device_id ||
+            $prev_guest['node_name'] != $node_name ||
+            $prev_guest['guest_name'] != $guest['name'] ||
+            $prev_guest['guest_type'] != $guest['type'])
+        {
+          // Prepare query params
+          $params = array('device_id' => $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 @@
+<?php
+
+/*
+
+Copyright (c) 2012 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.
+
+*/
+
+class PVE2_API {
+	private $constructor_success;
+
+	protected $pve_hostname;
+	protected $pve_username;
+	protected $pve_realm;
+	protected $pve_password;
+
+	private $print_debug;
+
+	protected $pve_login_ticket;
+	protected $pve_login_ticket_timestamp;
+	protected $pve_cluster_node_list;
+
+	public function __construct ($pve_hostname, $pve_username, $pve_realm, $pve_password) {
+		if (empty($pve_hostname) || empty($pve_username) || empty($pve_realm) || empty($pve_password)) {
+			# TODO - better error handling?
+			print("Error - Hostname/Username/Realm/Password required for PVE_API object constructor.\n");
+			$this->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.
+}
+
+?>